Run details
- Model
- minimax/minimax-m3.1-flash:free
- Route
- OpenRouter API, one user message
- Reasoning
- provider default, 29,223 reasoning tokens
- Limits
- 60 min · 524,288 output tokens
- Date
- 21 Oct 2026
- Time
- 8 min 7 s
- Size
- 57 kB
- Tokens
- 279 in, 47,868 out
- List price
- free (list price $0)
- Paid
- $0 (free route)
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
57 kB 2,046 lines Raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#070914">
<title>Neon Break</title>
<style>
:root {
color-scheme: dark;
--bg: #070914;
--panel: rgba(15, 20, 39, 0.86);
--line: rgba(139, 158, 255, 0.18);
--text: #f4f7ff;
--muted: #8e9ab8;
--cyan: #56efff;
--purple: #a879ff;
--pink: #ff4f9a;
--gold: #ffd166;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
min-height: 100%;
background: var(--bg);
}
body {
min-height: 100vh;
display: grid;
place-items: center;
padding: 20px;
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background:
radial-gradient(circle at 15% 5%, rgba(75, 67, 190, 0.22), transparent 34rem),
radial-gradient(circle at 90% 85%, rgba(0, 205, 225, 0.12), transparent 32rem),
#070914;
overflow-x: hidden;
}
button {
font: inherit;
}
[hidden] { display: none !important; }
.shell {
width: min(1040px, 100%);
display: grid;
gap: 14px;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.brand {
display: flex;
align-items: center;
gap: 11px;
min-width: max-content;
}
.brand-mark {
width: 37px;
height: 37px;
position: relative;
border-radius: 11px;
transform: rotate(-8deg);
background: linear-gradient(135deg, var(--cyan), var(--purple) 65%, var(--pink));
box-shadow: 0 0 25px rgba(86, 239, 255, 0.27);
}
.brand-mark::before,
.brand-mark::after {
content: "";
position: absolute;
left: 8px;
right: 8px;
height: 4px;
border-radius: 99px;
background: #07101e;
}
.brand-mark::before { top: 10px; }
.brand-mark::after { bottom: 10px; }
.brand h1 {
margin: 0;
font-size: 17px;
line-height: 1;
letter-spacing: 0.16em;
font-weight: 900;
}
.brand small {
display: block;
margin-top: 5px;
color: var(--muted);
font-size: 9px;
font-weight: 800;
letter-spacing: 0.2em;
}
.hud {
display: flex;
justify-content: flex-end;
align-items: stretch;
gap: 8px;
flex-wrap: wrap;
}
.stat {
min-width: 78px;
padding: 8px 12px;
border: 1px solid var(--line);
border-radius: 11px;
background: rgba(11, 15, 31, 0.72);
box-shadow: inset 0 1px rgba(255,255,255,0.025);
}
.stat span {
display: block;
color: var(--muted);
font-size: 8px;
font-weight: 900;
letter-spacing: 0.15em;
margin-bottom: 3px;
}
.stat strong {
display: block;
font-size: 15px;
line-height: 1;
font-variant-numeric: tabular-nums;
letter-spacing: 0.03em;
}
#scoreValue { color: var(--cyan); }
#livesValue { color: var(--pink); letter-spacing: 0.08em; }
#comboValue { color: var(--gold); }
#bestValue { color: #c4b5fd; }
.stage {
position: relative;
overflow: hidden;
border: 1px solid rgba(139, 158, 255, 0.28);
border-radius: 20px;
background: #050711;
box-shadow:
0 25px 80px rgba(0, 0, 0, 0.48),
0 0 0 1px rgba(255,255,255,0.025) inset,
0 0 70px rgba(81, 62, 181, 0.09);
isolation: isolate;
}
.stage::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border-radius: inherit;
box-shadow: inset 0 0 80px rgba(0,0,0,0.24);
z-index: 3;
}
canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 10;
touch-action: none;
cursor: crosshair;
outline: none;
}
.overlay {
position: absolute;
inset: 0;
z-index: 4;
display: grid;
place-items: center;
padding: 24px;
pointer-events: none;
background:
radial-gradient(circle at center, rgba(25, 18, 61, 0.35), rgba(3, 5, 13, 0.68));
opacity: 1;
visibility: visible;
transition: opacity 180ms ease, visibility 180ms ease;
}
.overlay.hidden {
opacity: 0;
visibility: hidden;
}
.overlay-card {
width: min(470px, 94%);
padding: 30px;
text-align: center;
border: 1px solid rgba(156, 177, 255, 0.24);
border-radius: 20px;
pointer-events: auto;
background:
linear-gradient(145deg, rgba(23, 29, 56, 0.96), rgba(9, 12, 27, 0.96));
box-shadow:
0 24px 70px rgba(0, 0, 0, 0.55),
0 0 55px rgba(92, 72, 205, 0.16),
inset 0 1px rgba(255,255,255,0.06);
animation: cardIn 320ms cubic-bezier(.2,.8,.2,1);
}
@keyframes cardIn {
from { opacity: 0; transform: translateY(12px) scale(0.96); }
to { opacity: 1; transform: none; }
}
.eyebrow {
color: var(--cyan);
font-size: 10px;
font-weight: 900;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.overlay-card h2 {
margin: 10px 0 9px;
font-size: clamp(30px, 5vw, 52px);
line-height: 0.98;
letter-spacing: -0.045em;
font-weight: 950;
background: linear-gradient(110deg, #fff 5%, #a9f8ff 42%, #c4a4ff 90%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.overlay-card p {
margin: 0 auto;
max-width: 390px;
color: #aeb8d3;
font-size: 14px;
line-height: 1.55;
white-space: pre-line;
}
.overlay-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin: 19px 0;
}
.overlay-stats div {
padding: 10px 7px;
border: 1px solid rgba(139, 158, 255, 0.13);
border-radius: 11px;
background: rgba(4, 7, 17, 0.48);
}
.overlay-stats span {
display: block;
color: var(--muted);
font-size: 8px;
font-weight: 900;
letter-spacing: 0.12em;
margin-bottom: 4px;
}
.overlay-stats strong {
color: #eefaff;
font-size: 16px;
}
.primary {
min-width: 190px;
margin-top: 20px;
padding: 13px 23px;
border: 0;
border-radius: 12px;
color: #06101c;
background: linear-gradient(100deg, #5df3ff, #b47aff 65%, #ff71ad);
box-shadow: 0 10px 30px rgba(88, 188, 255, 0.2);
font-size: 12px;
font-weight: 950;
letter-spacing: 0.1em;
cursor: pointer;
transition: transform 130ms ease, filter 130ms ease, box-shadow 130ms ease;
}
.primary:hover {
transform: translateY(-2px);
filter: brightness(1.08);
box-shadow: 0 13px 36px rgba(88, 188, 255, 0.3);
}
.primary:active { transform: translateY(1px) scale(0.98); }
.keyhint {
margin-top: 15px !important;
color: #707d9e !important;
font-size: 10px !important;
letter-spacing: 0.035em;
}
.underbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
min-height: 38px;
}
.boosts {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
}
.boost-label {
color: #626e8d;
font-size: 8px;
font-weight: 900;
letter-spacing: 0.15em;
}
.boost {
padding: 5px 8px;
border: 1px solid currentColor;
border-radius: 999px;
font-size: 9px;
font-weight: 900;
letter-spacing: 0.05em;
white-space: nowrap;
background: rgba(255,255,255,0.035);
}
.empty-boost {
color: #59647f;
font-size: 10px;
}
.controls {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 7px;
flex-wrap: wrap;
}
.control {
padding: 7px 10px;
border: 1px solid var(--line);
border-radius: 9px;
color: #aab5ce;
background: rgba(13, 17, 34, 0.78);
font-size: 9px;
font-weight: 850;
letter-spacing: 0.08em;
cursor: pointer;
transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
}
.control:hover:not(:disabled) {
color: white;
border-color: rgba(86, 239, 255, 0.42);
background: rgba(86, 239, 255, 0.07);
}
.control:disabled {
opacity: 0.35;
cursor: default;
}
kbd {
display: inline-block;
min-width: 18px;
padding: 2px 5px;
margin-right: 3px;
border: 1px solid #343d59;
border-bottom-width: 2px;
border-radius: 4px;
color: #dbe5ff;
background: #11172b;
font: 800 9px ui-monospace, SFMono-Regular, Menlo, monospace;
}
@media (max-width: 720px) {
body { padding: 10px; }
.topbar { align-items: flex-start; }
.brand h1 { font-size: 14px; }
.brand small { display: none; }
.brand-mark { width: 31px; height: 31px; border-radius: 9px; }
.brand-mark::before, .brand-mark::after { left: 6px; right: 6px; }
.stat { min-width: 61px; padding: 7px 8px; }
.stat strong { font-size: 12px; }
.hud { gap: 5px; }
.stage { border-radius: 14px; }
.underbar { align-items: flex-start; flex-direction: column; }
.controls { justify-content: flex-start; }
}
@media (max-width: 480px) {
.shell { gap: 9px; }
.topbar { flex-direction: column; }
.hud { width: 100%; justify-content: flex-start; }
.stat { flex: 1; min-width: 54px; }
.brand { display: none; }
.overlay { padding: 10px; }
.overlay-card { padding: 15px 17px; border-radius: 14px; }
.overlay-card h2 { margin: 6px 0; font-size: 28px; }
.overlay-card p { font-size: 11px; line-height: 1.4; }
.overlay-stats { margin: 10px 0; }
.overlay-stats div { padding: 6px 4px; }
.overlay-stats strong { font-size: 12px; }
.primary { margin-top: 10px; padding: 10px 18px; }
.keyhint { display: none; }
}
</style>
</head>
<body>
<main class="shell">
<header class="topbar">
<div class="brand">
<div class="brand-mark" aria-hidden="true"></div>
<div>
<h1>NEON BREAK</h1>
<small>SEVEN SECTORS // ONE BREAKOUT RUN</small>
</div>
</div>
<div class="hud" aria-label="Game statistics">
<div class="stat"><span>SCORE</span><strong id="scoreValue">0</strong></div>
<div class="stat"><span>SECTOR</span><strong id="levelValue">1 / 7</strong></div>
<div class="stat"><span>LIVES</span><strong id="livesValue">♥♥♥</strong></div>
<div class="stat"><span>CHAIN</span><strong id="comboValue">—</strong></div>
<div class="stat"><span>BEST</span><strong id="bestValue">0</strong></div>
</div>
</header>
<section class="stage" id="stage">
<canvas id="game" width="960" height="600" tabindex="0"
aria-label="Neon Breakout game. Move with the mouse or arrow keys, and press Space or click to launch."></canvas>
<div class="overlay" id="overlay">
<div class="overlay-card">
<div class="eyebrow" id="overlayTag">ARCADE // 7 SECTORS</div>
<h2 id="overlayTitle">NEON BREAK</h2>
<p id="overlayText">Smash every destructible tile, catch power-ups, and chain hits for a massive score.</p>
<div class="overlay-stats" id="overlayStats" hidden></div>
<button class="primary" id="overlayButton" type="button">START GAME</button>
<p class="keyhint" id="overlayHint">Mouse or ← → to move · Space / Click to launch · P to pause</p>
</div>
</div>
</section>
<footer class="underbar">
<div class="boosts">
<span class="boost-label">ACTIVE</span>
<div id="boostList"><span class="empty-boost">No active boosts</span></div>
</div>
<div class="controls">
<span class="control"><kbd>←</kbd><kbd>→</kbd> MOVE</span>
<span class="control"><kbd>SPACE</kbd> LAUNCH</span>
<button class="control" id="pauseBtn" type="button">PAUSE</button>
<button class="control" id="soundBtn" type="button" aria-pressed="true">SOUND ON</button>
<button class="control" id="resetBtn" type="button">RESTART</button>
</div>
</footer>
</main>
<script>
(() => {
"use strict";
const W = 960;
const H = 600;
const TAU = Math.PI * 2;
const BASE_PADDLE_W = 126;
const BRICK_W = 68;
const BRICK_H = 25;
const BRICK_GAP = 18;
const ROW_GAP = 10;
const BRICK_LEFT = 59;
const BRICK_TOP = 78;
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const els = {
score: document.getElementById("scoreValue"),
level: document.getElementById("levelValue"),
lives: document.getElementById("livesValue"),
combo: document.getElementById("comboValue"),
best: document.getElementById("bestValue"),
boostList: document.getElementById("boostList"),
overlay: document.getElementById("overlay"),
overlayTag: document.getElementById("overlayTag"),
overlayTitle: document.getElementById("overlayTitle"),
overlayText: document.getElementById("overlayText"),
overlayStats: document.getElementById("overlayStats"),
overlayButton: document.getElementById("overlayButton"),
overlayHint: document.getElementById("overlayHint"),
pause: document.getElementById("pauseBtn"),
sound: document.getElementById("soundBtn"),
reset: document.getElementById("resetBtn")
};
const BALL_COLORS = ["#62f3ff", "#ff67ad", "#ffd166", "#a879ff", "#75ffbc", "#ff8b5c"];
const POWER = {
wide: { symbol: "W", color: "#62f3ff", label: "WIDE PADDLE" },
multi: { symbol: "M", color: "#ff67ad", label: "MULTIBALL" },
slow: { symbol: "S", color: "#a879ff", label: "SLOW TIME" },
life: { symbol: "+", color: "#75ffbc", label: "EXTRA LIFE" }
};
const levels = [
{
name: "FIRST LIGHT",
note: "Warm up your paddle. Learn the bounce.",
accent: "#56efff",
build() {
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 10; c++) {
const hp = r === 1 && c >= 3 && c <= 6 ? 2 : 1;
addBrick(c, r, hp);
}
}
}
},
{
name: "PURPLE DESCENT",
note: "A tight pyramid. Keep your angles clean.",
accent: "#a879ff",
build() {
for (let r = 0; r < 5; r++) {
const count = 10 - r * 2;
const start = r;
for (let i = 0; i < count; i++) {
addBrick(start + i, r, i === 0 || i === count - 1 ? 2 : 1);
}
}
}
},
{
name: "IRON FORTRESS",
note: "Steel cannot be broken. Aim around it.",
accent: "#ffad42",
build() {
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 10; c++) {
addBrick(c, r, r === 1 && c >= 3 && c <= 6 ? 2 : 1);
}
}
addBrick(0, 3, 0, "steel");
for (let c = 1; c <= 8; c++) addBrick(c, 3, 2);
addBrick(9, 3, 0, "steel");
for (let c = 0; c < 10; c++) addBrick(c, 4, c === 0 || c === 9 ? 3 : 2);
addBrick(0, 5, 0, "steel");
addBrick(1, 5, 3);
for (let c = 2; c <= 7; c++) addBrick(c, 5, 2);
addBrick(8, 5, 3);
addBrick(9, 5, 0, "steel");
}
},
{
name: "CHECKER PULSE",
note: "Find the gaps. Every opening is an opportunity.",
accent: "#ff4f9a",
build() {
for (let r = 0; r < 6; r++) {
for (let c = 0; c < 10; c++) {
if ((r + c) % 2 === 0) {
addBrick(c, r, (r + c) % 4 === 0 ? 3 : 1);
}
}
}
addBrick(0, 1, 0, "steel");
addBrick(9, 0, 0, "steel");
}
},
{
name: "THE VAULT",
note: "A steel shell protects a volatile core.",
accent: "#75ffbc",
build() {
for (let c = 0; c < 10; c++) addBrick(c, 0, 0, "steel");
for (let r = 1; r <= 4; r++) {
addBrick(0, r, 0, "steel");
addBrick(9, r, 0, "steel");
for (let c = 1; c <= 8; c++) {
addBrick(c, r, (r + c) % 3 === 0 ? 3 : 2);
}
}
for (let c = 1; c <= 8; c++) addBrick(c, 5, 1);
}
},
{
name: "NEON GAUNTLET",
note: "A dense final defense. Keep the chain alive.",
accent: "#ff8b5c",
build() {
for (let r = 0; r < 5; r++) {
for (let c = 0; c < 10; c++) {
if (r % 2 === 1 && c % 5 === 0) {
addBrick(c, r, 0, "steel");
} else {
const hp = c === 2 || c === 7 ? 3 : (r === 3 ? 2 : 1);
addBrick(c, r, hp);
}
}
}
}
},
{
name: "THE CORE",
note: "One last shot at the heart of the system.",
accent: "#ff386c",
build() {
for (let c = 1; c <= 8; c++) addBrick(c, 0, 1);
for (let c = 2; c <= 7; c++) addBrick(c, 1, 2);
for (let c = 1; c <= 8; c++) addBrick(c, 2, 2);
addBrick(3, 3, 30, "boss", 4);
addBrick(1, 4, 3);
addBrick(2, 4, 2);
addBrick(7, 4, 2);
addBrick(8, 4, 3);
for (let c = 0; c < 10; c++) addBrick(c, 5, c === 0 || c === 9 ? 0 : 1, c === 0 || c === 9 ? "steel" : "normal");
}
}
];
let state = "title";
let levelIndex = 0;
let score = 0;
let lives = 3;
let best = 0;
let combo = 0;
let comboTimer = 0;
let levelTotal = 0;
let destructibleLeft = 0;
let lastBonus = 0;
try {
best = Number(localStorage.getItem("neon-break-best") || 0) || 0;
} catch (_) {}
let bricks = [];
let balls = [];
let powerups = [];
let particles = [];
let rings = [];
let popups = [];
const paddle = {
x: (W - BASE_PADDLE_W) / 2,
y: 538,
w: BASE_PADDLE_W,
h: 18,
vx: 0,
flash: 0
};
let wideTimer = 0;
let slowTimer = 0;
let deathTimer = 0;
let pausedFrom = "playing";
let shake = 0;
let flash = 0;
let hitStop = 0;
let visualTime = 0;
let toast = null;
let levelBanner = null;
let boostSignature = "";
let ballSerial = 0;
let overlayAction = null;
const keys = new Set();
const mouse = { active: false, center: W / 2 };
let inputMode = "keyboard";
const stars = Array.from({ length: 55 }, () => ({
x: Math.random() * W,
y: Math.random() * H,
r: Math.random() * 1.5 + 0.35,
phase: Math.random() * TAU,
speed: Math.random() * 0.8 + 0.25
}));
let dpr = 1;
function resizeCanvas() {
dpr = Math.min(2, window.devicePixelRatio || 1);
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function alphaColor(hex, alpha) {
const n = parseInt(hex.slice(1), 16);
return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${alpha})`;
}
function formatScore(n) {
return n.toLocaleString("en-US");
}
function roundRectPath(context, x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
context.beginPath();
context.moveTo(x + r, y);
context.arcTo(x + w, y, x + w, y + h, r);
context.arcTo(x + w, y + h, x, y + h, r);
context.arcTo(x, y + h, x, y, r);
context.arcTo(x, y, x + w, y, r);
context.closePath();
}
function addBrick(c, r, hp, type = "normal", span = 1) {
let color = "#56efff";
if (type === "steel") color = "#66738d";
else if (type === "boss") color = "#ff386c";
else if (hp >= 10) color = "#ff386c";
else if (hp >= 4) color = "#ff4f9a";
else if (hp === 3) color = "#ffad42";
else if (hp === 2) color = "#a879ff";
bricks.push({
x: BRICK_LEFT + c * (BRICK_W + BRICK_GAP),
y: BRICK_TOP + r * (BRICK_H + ROW_GAP),
w: span * BRICK_W + (span - 1) * BRICK_GAP,
h: BRICK_H,
hp,
maxHp: hp,
type,
color,
alive: true,
destructible: type !== "steel",
flash: 0,
cooldown: 0,
id: bricks.length
});
}
function loadBricks(index) {
bricks = [];
levels[index].build();
levelTotal = bricks.filter(b => b.destructible).length;
destructibleLeft = levelTotal;
}
function makeBall(x, y) {
const id = ballSerial++;
return {
id,
x,
y,
px: x,
py: y,
vx: 0,
vy: 0,
r: 8,
speed: 480,
launched: false,
dead: false,
trail: [],
color: BALL_COLORS[id % BALL_COLORS.length]
};
}
function prepareServe() {
paddle.w = BASE_PADDLE_W;
paddle.x = (W - paddle.w) / 2;
paddle.vx = 0;
paddle.flash = 0;
balls = [makeBall(W / 2, paddle.y - 12)];
powerups = [];
combo = 0;
comboTimer = 0;
wideTimer = 0;
slowTimer = 0;
state = "serve";
updateHud();
}
function loadLevel(index) {
levelIndex = index;
loadBricks(index);
prepareServe();
levelBanner = {
title: levels[index].name,
subtitle: levels[index].note,
life: 1.8,
max: 1.8
};
}
function startGame() {
ensureAudio();
score = 0;
lives = 3;
combo = 0;
comboTimer = 0;
particles = [];
rings = [];
popups = [];
shake = 0;
flash = 0;
hitStop = 0;
toast = null;
loadLevel(0);
hideOverlay();
}
function nextLevel() {
if (levelIndex >= levels.length - 1) {
showVictory();
return;
}
loadLevel(levelIndex + 1);
hideOverlay();
}
function showOverlay(options) {
els.overlay.classList.remove("hidden");
els.overlayTag.textContent = options.tag || "NEON BREAK";
els.overlayTitle.textContent = options.title || "";
els.overlayText.textContent = options.text || "";
els.overlayButton.textContent = options.button || "CONTINUE";
els.overlayHint.textContent = options.hint || "Mouse or ← → to move · Space / Click to launch";
els.overlayStats.hidden = !options.stats;
els.overlayStats.innerHTML = options.stats || "";
overlayAction = options.action || null;
}
function hideOverlay() {
els.overlay.classList.add("hidden");
overlayAction = null;
}
function showTitle() {
state = "title";
showOverlay({
tag: "ARCADE // 7 SECTORS",
title: "NEON BREAK",
text: "Smash every destructible tile, catch power-ups, and chain hits for a massive score.",
button: "START GAME",
stats: `<div><span>BEST</span><strong>${formatScore(best)}</strong></div>
<div><span>SECTORS</span><strong>7</strong></div>
<div><span>POWER-UPS</span><strong>4</strong></div>`,
action: startGame,
hint: "Mouse or ← → to move · Space / Click to launch · P to pause"
});
}
function showPause() {
showOverlay({
tag: "SIMULATION PAUSED",
title: "TAKE A BREATH",
text: "The matrix can wait. Your run is safe.",
button: "RESUME",
action: resumeGame,
hint: "Press P, Space, or click Resume"
});
}
function showLevelClear() {
showOverlay({
tag: `SECTOR ${String(levelIndex + 1).padStart(2, "0")} COMPLETE`,
title: "CLEAN SWEEP.",
text: `Bonus awarded: ${formatScore(lastBonus)} points.\nThe next sector is already waiting.`,
button: levelIndex === levels.length - 1 ? "CLAIM VICTORY" : "NEXT SECTOR →",
stats: `<div><span>SCORE</span><strong>${formatScore(score)}</strong></div>
<div><span>BONUS</span><strong>+${formatScore(lastBonus)}</strong></div>
<div><span>LIVES</span><strong>${lives}</strong></div>`,
action: nextLevel,
hint: "Space / Click to continue"
});
}
function showGameOver() {
showOverlay({
tag: "RUN TERMINATED",
title: "GAME OVER",
text: "The matrix wins this round. Reset the system and go again.",
button: "PLAY AGAIN",
stats: `<div><span>SCORE</span><strong>${formatScore(score)}</strong></div>
<div><span>BEST</span><strong>${formatScore(best)}</strong></div>
<div><span>SECTOR</span><strong>${levelIndex + 1}</strong></div>`,
action: startGame,
hint: "Space / Click / R to restart"
});
}
function showVictory() {
state = "victory";
saveBest();
showOverlay({
tag: "ALL SECTORS CLEARED",
title: "SYSTEM BROKEN.",
text: "You reached the Core and brought the whole neon grid down. That was a beautiful run.",
button: "RUN IT BACK",
stats: `<div><span>FINAL SCORE</span><strong>${formatScore(score)}</strong></div>
<div><span>BEST</span><strong>${formatScore(best)}</strong></div>
<div><span>LIVES</span><strong>${lives}</strong></div>`,
action: startGame,
hint: "Space / Click / R to play again"
});
}
function saveBest() {
try {
localStorage.setItem("neon-break-best", String(best));
} catch (_) {}
}
function togglePause() {
if (state === "playing" || state === "serve") {
pausedFrom = state;
state = "paused";
showPause();
updateHud();
} else if (state === "paused") {
resumeGame();
}
}
function resumeGame() {
state = pausedFrom;
hideOverlay();
updateHud();
}
function activatePrimary() {
if (state === "title" || state === "gameover" || state === "victory") startGame();
else if (state === "serve") launchBalls();
else if (state === "paused") resumeGame();
else if (state === "levelclear") nextLevel();
}
function launchBalls() {
if (state !== "serve") return;
for (const b of balls) {
if (!b.launched) {
const offset = Math.random() * 0.52 - 0.26;
b.launched = true;
b.vx = Math.sin(offset) * b.speed;
b.vy = -Math.cos(offset) * b.speed;
}
}
state = "playing";
sfx("launch");
}
function addScore(amount) {
score += Math.max(0, amount);
if (score > best) best = score;
updateHud();
}
function comboMultiplier() {
return Math.min(5, 1 + Math.floor(combo / 2));
}
function loseLife() {
if (state !== "playing") return;
lives--;
updateHud();
if (lives <= 0) {
state = "gameover";
saveBest();
sfx("lose");
showGameOver();
} else {
state = "dying";
deathTimer = 1.05;
powerups = [];
sfx("lose");
}
}
function completeLevel() {
if (state !== "playing") return;
lastBonus = 800 + levelIndex * 250 + lives * 125;
addScore(lastBonus);
spawnConfetti(90);
sfx("clear");
if (levelIndex === levels.length - 1) {
showVictory();
} else {
state = "levelclear";
showLevelClear();
}
}
function burst(x, y, color, count = 10, force = 190, confetti = false) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * TAU;
const speed = force * (0.35 + Math.random() * 0.85);
const life = 0.35 + Math.random() * (confetti ? 1.1 : 0.55);
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - (confetti ? 80 : 0),
gravity: confetti ? 260 : 95,
life,
maxLife: life,
size: confetti ? 3 + Math.random() * 5 : 1.5 + Math.random() * 3.5,
color,
rot: Math.random() * TAU,
spin: (Math.random() - 0.5) * 10,
type: confetti ? "confetti" : "spark"
});
}
if (particles.length > 650) particles.splice(0, particles.length - 650);
}
function spawnConfetti(count) {
const colors = ["#56efff", "#a879ff", "#ff4f9a", "#ffd166", "#75ffbc"];
for (let i = 0; i < count; i++) {
burst(Math.random() * W, -10 - Math.random() * 120, colors[i % colors.length], 1, 60, true);
}
}
function spawnRing(x, y, color, maxRadius = 45) {
rings.push({ x, y, color, radius: 5, maxRadius, life: 0.35, maxLife: 0.35 });
}
function spawnPopup(x, y, text, color = "#ffffff") {
popups.push({ x, y, text, color, life: 0.9, maxLife: 0.9 });
}
function showToast(text, color = "#ffffff") {
toast = { text, color, life: 1.45, max: 1.45 };
}
function maybeDropPowerup(brick) {
const chance = 0.14 + Math.min(0.08, combo * 0.01);
if (Math.random() > chance) return;
const roll = Math.random();
let kind = "wide";
if (roll >= 0.30 && roll < 0.62) kind = "multi";
else if (roll >= 0.62 && roll < 0.91) kind = "slow";
else if (roll >= 0.91) kind = "life";
powerups.push({
x: Math.max(18, Math.min(W - 46, brick.x + brick.w / 2 - 14)),
y: brick.y + brick.h,
w: 28,
h: 28,
vy: 125,
rotation: 0,
kind
});
}
function applyPowerup(power) {
const config = POWER[power.kind];
addScore(100);
showToast(config.label, config.color);
burst(power.x + 14, power.y + 14, config.color, 20, 230);
spawnRing(power.x + 14, power.y + 14, config.color, 70);
sfx("power");
if (power.kind === "wide") {
wideTimer = 12;
} else if (power.kind === "slow") {
slowTimer = 8;
} else if (power.kind === "life") {
lives++;
} else if (power.kind === "multi") {
spawnMultiball();
}
updateHud();
}
function spawnMultiball() {
const source = balls.find(b => b.launched) || balls[0];
if (!source) return;
const baseAngle = Math.atan2(source.vy, source.vx);
for (const offset of [-0.58, 0.58]) {
if (balls.length >= 7) break;
const b = makeBall(source.x, source.y);
const angle = baseAngle + offset;
b.launched = true;
b.speed = source.speed;
b.vx = Math.cos(angle) * b.speed;
b.vy = Math.sin(angle) * b.speed;
balls.push(b);
}
spawnRing(source.x, source.y, "#ff67ad", 90);
}
function hitBrick(brick, ball) {
brick.flash = 0.16;
if (brick.type === "steel") {
burst(ball.x, ball.y, "#c7d3ea", 6, 135);
shake = Math.max(shake, 1.2);
sfx("steel");
return;
}
brick.hp--;
addScore(15 * comboMultiplier());
burst(ball.x, ball.y, brick.color, 5, 120);
if (brick.hp <= 0) {
brick.alive = false;
destructibleLeft = Math.max(0, destructibleLeft - 1);
if (comboTimer > 0) combo = Math.min(99, combo + 1);
else combo = 1;
comboTimer = 2.7;
const multiplier = comboMultiplier();
const points = 100 * brick.maxHp * multiplier;
addScore(points);
const cx = brick.x + brick.w / 2;
const cy = brick.y + brick.h / 2;
spawnPopup(cx, cy, `+${formatScore(points)}`, brick.color);
spawnRing(cx, cy, brick.color, brick.type === "boss" ? 180 : 48);
burst(cx, cy, brick.color, brick.type === "boss" ? 65 : 15, brick.type === "boss" ? 360 : 220);
shake = Math.max(shake, brick.type === "boss" ? 16 : 3.2);
flash = Math.max(flash, brick.type === "boss" ? 0.2 : 0.045);
hitStop = Math.max(hitStop, brick.type === "boss" ? 0.065 : 0.016);
ball.speed = Math.min(620, ball.speed + 2.5);
maybeDropPowerup(brick);
sfx("destroy");
if (combo > 1 && combo % 5 === 0) {
showToast(`${combo} HIT STREAK!`, "#ffd166");
}
} else {
shake = Math.max(shake, 0.8);
sfx("brick");
}
updateHud();
}
function currentBallSpeed(ball) {
return ball.speed * (slowTimer > 0 ? 0.68 : 1);
}
function normalizeBall(ball) {
const speed = currentBallSpeed(ball);
let mag = Math.hypot(ball.vx, ball.vy);
if (!isFinite(mag) || mag < 0.001) {
ball.vx = 0;
ball.vy = -speed;
mag = speed;
}
ball.vx = ball.vx / mag * speed;
ball.vy = ball.vy / mag * speed;
if (Math.abs(ball.vy) < speed * 0.2) {
const signY = ball.vy < 0 ? -1 : 1;
const signX = ball.vx === 0 ? (Math.random() < 0.5 ? -1 : 1) : Math.sign(ball.vx);
ball.vy = signY * speed * 0.2;
ball.vx = signX * Math.sqrt(Math.max(0, speed * speed - ball.vy * ball.vy));
}
}
function collideWithBrick(ball, brick) {
const closestX = Math.max(brick.x, Math.min(ball.x, brick.x + brick.w));
const closestY = Math.max(brick.y, Math.min(ball.y, brick.y + brick.h));
const dx = ball.x - closestX;
const dy = ball.y - closestY;
const distSq = dx * dx + dy * dy;
if (distSq > ball.r * ball.r) return false;
let nx, ny, penetration;
const dist = Math.sqrt(distSq);
if (dist > 0.0001) {
nx = dx / dist;
ny = dy / dist;
penetration = ball.r - dist;
} else {
const left = ball.x - brick.x;
const right = brick.x + brick.w - ball.x;
const top = ball.y - brick.y;
const bottom = brick.y + brick.h - ball.y;
const smallest = Math.min(left, right, top, bottom);
if (smallest === left) { nx = -1; ny = 0; penetration = left + ball.r; }
else if (smallest === right) { nx = 1; ny = 0; penetration = right + ball.r; }
else if (smallest === top) { nx = 0; ny = -1; penetration = top + ball.r; }
else { nx = 0; ny = 1; penetration = bottom + ball.r; }
}
ball.x += nx * (penetration + 0.25);
ball.y += ny * (penetration + 0.25);
const dot = ball.vx * nx + ball.vy * ny;
if (dot < 0) {
ball.vx -= 2 * dot * nx;
ball.vy -= 2 * dot * ny;
}
return true;
}
function updateBall(ball, dt) {
if (!ball.launched) {
ball.x = paddle.x + paddle.w / 2;
ball.y = paddle.y - ball.r - 2;
ball.trail.length = 0;
return;
}
ball.px = ball.x;
ball.py = ball.y;
normalizeBall(ball);
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
ball.trail.push({ x: ball.x, y: ball.y });
if (ball.trail.length > 11) ball.trail.shift();
if (ball.x - ball.r < 0) {
ball.x = ball.r;
ball.vx = Math.abs(ball.vx);
sfx("wall");
} else if (ball.x + ball.r > W) {
ball.x = W - ball.r;
ball.vx = -Math.abs(ball.vx);
sfx("wall");
}
if (ball.y - ball.r < 0) {
ball.y = ball.r;
ball.vy = Math.abs(ball.vy);
sfx("wall");
}
const paddleCenter = paddle.x + paddle.w / 2;
const previousBottom = ball.py + ball.r;
const overlapsPaddle =
ball.x + ball.r >= paddle.x &&
ball.x - ball.r <= paddle.x + paddle.w;
if (ball.vy > 0 && previousBottom <= paddle.y + 9 && ball.y + ball.r >= paddle.y && overlapsPaddle) {
const offset = Math.max(-1, Math.min(1, (ball.x - paddleCenter) / (paddle.w / 2)));
const angle = offset * 1.12 + Math.max(-0.12, Math.min(0.12, paddle.vx / 1200));
const speed = currentBallSpeed(ball);
ball.vx = Math.sin(angle) * speed + paddle.vx * 0.10;
ball.vy = -Math.abs(Math.cos(angle) * speed);
ball.y = paddle.y - ball.r - 0.5;
paddle.flash = 0.14;
burst(ball.x, ball.y, "#8df7ff", 6, 120);
sfx("paddle");
}
for (const brick of bricks) {
if (!brick.alive || brick.cooldown > 0) continue;
if (collideWithBrick(ball, brick)) {
brick.cooldown = 0.055;
hitBrick(brick, ball);
break;
}
}
normalizeBall(ball);
if (ball.y - ball.r > H + 8) {
ball.dead = true;
burst(ball.x, H - 4, "#ff4f9a", 13, 220);
}
}
function updateBalls(dt) {
balls = balls.filter(b => !b.dead);
const steps = 2;
const subDt = dt / steps;
for (let s = 0; s < steps; s++) {
for (const ball of balls.slice()) {
if (!ball.dead) updateBall(ball, subDt);
}
}
balls = balls.filter(b => !b.dead);
}
function updatePaddle(dt) {
const desiredWidth = BASE_PADDLE_W + (wideTimer > 0 ? 54 : 0);
paddle.w += (desiredWidth - paddle.w) * (1 - Math.exp(-11 * dt));
const left = keys.has("ArrowLeft") || keys.has("KeyA");
const right = keys.has("ArrowRight") || keys.has("KeyD");
const axis = (right ? 1 : 0) - (left ? 1 : 0);
const oldCenter = paddle.x + paddle.w / 2;
let center = oldCenter;
if (axis !== 0) {
inputMode = "keyboard";
center += axis * 720 * dt;
} else if (inputMode === "mouse" && mouse.active) {
center += (mouse.center - center) * (1 - Math.exp(-24 * dt));
}
center = Math.max(paddle.w / 2, Math.min(W - paddle.w / 2, center));
paddle.x = center - paddle.w / 2;
paddle.vx = dt > 0 ? (center - oldCenter) / dt : 0;
}
function updatePowerDrops(dt) {
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.y += p.vy * dt;
p.rotation += dt * 2.2;
const caught =
p.y + p.h >= paddle.y &&
p.y <= paddle.y + paddle.h &&
p.x + p.w >= paddle.x &&
p.x <= paddle.x + paddle.w;
if (caught) {
applyPowerup(p);
powerups.splice(i, 1);
} else if (p.y > H + 35) {
powerups.splice(i, 1);
}
}
}
function updateTimers(dt) {
wideTimer = Math.max(0, wideTimer - dt);
slowTimer = Math.max(0, slowTimer - dt);
const sig = `${Math.ceil(wideTimer)}:${Math.ceil(slowTimer)}`;
if (sig !== boostSignature) updateHud();
}
function updateEffects(dt) {
for (const p of particles) {
p.life -= dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += p.gravity * dt;
p.vx *= Math.exp(-1.8 * dt);
p.rot += p.spin * dt;
}
particles = particles.filter(p => p.life > 0);
for (const r of rings) {
r.life -= dt;
r.radius += (r.maxRadius - r.radius) * (1 - Math.exp(-10 * dt));
}
rings = rings.filter(r => r.life > 0);
for (const p of popups) {
p.life -= dt;
p.y -= 36 * dt;
}
popups = popups.filter(p => p.life > 0);
for (const b of bricks) {
b.flash = Math.max(0, b.flash - dt);
b.cooldown = Math.max(0, b.cooldown - dt);
}
paddle.flash = Math.max(0, paddle.flash - dt);
shake *= Math.exp(-13 * dt);
flash *= Math.exp(-10 * dt);
if (toast) {
toast.life -= dt;
if (toast.life <= 0) toast = null;
}
if (levelBanner) {
levelBanner.life -= dt;
if (levelBanner.life <= 0) levelBanner = null;
}
}
function updatePlaying(dt) {
if (comboTimer > 0) {
comboTimer -= dt;
if (comboTimer <= 0) {
combo = 0;
comboTimer = 0;
updateHud();
}
}
updateTimers(dt);
updatePaddle(dt);
updateBalls(dt);
updatePowerDrops(dt);
if (destructibleLeft <= 0) {
completeLevel();
} else if (balls.length === 0) {
loseLife();
}
}
function update(dt) {
visualTime += dt;
if (state === "paused") return;
updateEffects(dt);
if (state === "playing") {
updatePlaying(dt);
} else if (state === "serve") {
updatePaddle(dt);
for (const ball of balls) {
if (!ball.launched) {
ball.x = paddle.x + paddle.w / 2;
ball.y = paddle.y - ball.r - 2;
}
}
} else if (state === "dying") {
deathTimer -= dt;
if (deathTimer <= 0) prepareServe();
}
}
function updateHud() {
els.score.textContent = formatScore(score);
els.level.textContent = `${levelIndex + 1} / ${levels.length}`;
els.lives.textContent = "♥".repeat(Math.max(0, Math.min(3, lives))) + "♡".repeat(Math.max(0, 3 - Math.min(3, lives)));
els.combo.textContent = comboTimer > 0 ? `×${comboMultiplier()}` : "—";
els.best.textContent = formatScore(best);
const boosts = [];
if (wideTimer > 0) boosts.push(`<span class="boost" style="color:#62f3ff">WIDE ${Math.ceil(wideTimer)}s</span>`);
if (slowTimer > 0) boosts.push(`<span class="boost" style="color:#a879ff">SLOW ${Math.ceil(slowTimer)}s</span>`);
if (comboTimer > 0) boosts.push(`<span class="boost" style="color:#ffd166">CHAIN ×${comboMultiplier()}</span>`);
els.boostList.innerHTML = boosts.length ? boosts.join("") : '<span class="empty-boost">No active boosts</span>';
boostSignature = `${Math.ceil(wideTimer)}:${Math.ceil(slowTimer)}`;
els.pause.disabled = !(state === "playing" || state === "serve" || state === "paused");
els.pause.textContent = state === "paused" ? "RESUME" : "PAUSE";
}
function drawBackground() {
const accent = levels[levelIndex].accent;
ctx.fillStyle = "#050713";
ctx.fillRect(0, 0, W, H);
const glow = ctx.createRadialGradient(W * 0.5, 80, 15, W * 0.5, H * 0.52, W * 0.72);
glow.addColorStop(0, alphaColor(accent, 0.16));
glow.addColorStop(0.42, "rgba(16, 19, 43, 0.72)");
glow.addColorStop(1, "rgba(3, 5, 13, 0)");
ctx.fillStyle = glow;
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.strokeStyle = "rgba(124, 144, 213, 0.065)";
ctx.lineWidth = 1;
const grid = 48;
const offset = (visualTime * 9) % grid;
for (let x = -grid + offset; x < W + grid; x += grid) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, H);
ctx.stroke();
}
for (let y = -grid + offset; y < H + grid; y += grid) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
ctx.restore();
for (const star of stars) {
const twinkle = 0.25 + 0.55 * (0.5 + 0.5 * Math.sin(visualTime * star.speed + star.phase));
ctx.globalAlpha = twinkle;
ctx.fillStyle = "#dceaff";
ctx.beginPath();
ctx.arc(star.x, star.y, star.r, 0, TAU);
ctx.fill();
}
ctx.globalAlpha = 1;
const floor = ctx.createLinearGradient(0, H - 130, 0, H);
floor.addColorStop(0, "rgba(5, 7, 19, 0)");
floor.addColorStop(1, "rgba(4, 5, 14, 0.72)");
ctx.fillStyle = floor;
ctx.fillRect(0, H - 130, W, 130);
}
function drawBricks() {
for (const b of bricks) {
if (!b.alive) continue;
ctx.save();
const pulse = b.type === "boss" ? 0.75 + Math.sin(visualTime * 7) * 0.25 : 1;
ctx.shadowColor = b.color;
ctx.shadowBlur = b.flash > 0 ? 28 : b.type === "boss" ? 22 : 8;
let gradient = ctx.createLinearGradient(b.x, b.y, b.x, b.y + b.h);
if (b.type === "steel") {
gradient.addColorStop(0, "#8390a8");
gradient.addColorStop(0.5, "#465269");
gradient.addColorStop(1, "#242c40");
} else if (b.type === "boss") {
gradient.addColorStop(0, `rgba(255, 70, 119, ${pulse})`);
gradient.addColorStop(0.52, "#8d1747");
gradient.addColorStop(1, "#250b29");
} else {
gradient.addColorStop(0, b.color);
gradient.addColorStop(1, "#151a35");
}
roundRectPath(ctx, b.x, b.y, b.w, b.h, 5);
ctx.fillStyle = gradient;
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "rgba(255,255,255,0.18)";
roundRectPath(ctx, b.x + 3, b.y + 3, b.w - 6, 5, 2);
ctx.fill();
ctx.fillStyle = "rgba(0,0,0,0.2)";
roundRectPath(ctx, b.x + 4, b.y + b.h - 6, b.w - 8, 3, 1.5);
ctx.fill();
if (b.type === "steel") {
ctx.save();
roundRectPath(ctx, b.x, b.y, b.w, b.h, 5);
ctx.clip();
ctx.strokeStyle = "rgba(255,255,255,0.13)";
ctx.lineWidth = 5;
for (let x = b.x - b.h; x < b.x + b.w + b.h; x += 17) {
ctx.beginPath();
ctx.moveTo(x, b.y + b.h);
ctx.lineTo(x + b.h, b.y);
ctx.stroke();
}
ctx.restore();
ctx.fillStyle = "#c7d1e3";
for (const rx of [b.x + 7, b.x + b.w - 7]) {
ctx.beginPath();
ctx.arc(rx, b.y + b.h / 2, 1.8, 0, TAU);
ctx.fill();
}
}
if (b.destructible && b.hp < b.maxHp) {
const cracks = Math.min(5, b.maxHp - b.hp + 1);
ctx.strokeStyle = "rgba(7, 8, 18, 0.58)";
ctx.lineWidth = 1.7;
ctx.beginPath();
for (let i = 0; i < cracks; i++) {
const x = b.x + b.w * (0.2 + i * 0.16);
ctx.moveTo(x, b.y + 2);
ctx.lineTo(x - 4, b.y + b.h * 0.48);
ctx.lineTo(x + 3, b.y + b.h - 2);
}
ctx.stroke();
}
if (b.type === "boss") {
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "900 15px system-ui, sans-serif";
ctx.fillStyle = "#fff0f5";
ctx.fillText("THE CORE", b.x + b.w / 2, b.y + b.h / 2 - 2);
ctx.fillStyle = "rgba(10,5,18,0.55)";
ctx.fillRect(b.x + 10, b.y + b.h - 7, b.w - 20, 3);
ctx.fillStyle = "#ff7ba9";
ctx.fillRect(b.x + 10, b.y + b.h - 7, (b.w - 20) * Math.max(0, b.hp / b.maxHp), 3);
}
if (b.flash > 0) {
ctx.globalAlpha = Math.min(1, b.flash / 0.16);
ctx.fillStyle = "#ffffff";
roundRectPath(ctx, b.x, b.y, b.w, b.h, 5);
ctx.fill();
}
ctx.restore();
}
}
function drawPaddle() {
ctx.save();
const gradient = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.h);
gradient.addColorStop(0, wideTimer > 0 ? "#fff4bd" : "#b8fbff");
gradient.addColorStop(0.42, wideTimer > 0 ? "#ffd166" : "#56efff");
gradient.addColorStop(1, wideTimer > 0 ? "#d97832" : "#7b65ff");
ctx.shadowColor = wideTimer > 0 ? "#ffd166" : "#56efff";
ctx.shadowBlur = paddle.flash > 0 ? 35 : 21;
roundRectPath(ctx, paddle.x, paddle.y, paddle.w, paddle.h, 9);
ctx.fillStyle = gradient;
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "rgba(255,255,255,0.68)";
roundRectPath(ctx, paddle.x + 11, paddle.y + 3, paddle.w - 22, 4, 2);
ctx.fill();
if (wideTimer > 0) {
ctx.strokeStyle = `rgba(255, 238, 150, ${0.55 + Math.sin(visualTime * 12) * 0.25})`;
ctx.lineWidth = 2;
roundRectPath(ctx, paddle.x - 2, paddle.y - 2, paddle.w + 4, paddle.h + 4, 10);
ctx.stroke();
}
ctx.restore();
}
function drawBalls() {
for (const ball of balls) {
for (let i = 0; i < ball.trail.length; i++) {
const point = ball.trail[i];
const t = (i + 1) / ball.trail.length;
ctx.globalAlpha = t * 0.28;
ctx.fillStyle = ball.color;
ctx.beginPath();
ctx.arc(point.x, point.y, ball.r * (0.25 + t * 0.75), 0, TAU);
ctx.fill();
}
ctx.globalAlpha = 1;
const pulse = ball.launched ? 1 : 1 + Math.sin(visualTime * 8) * 0.09;
ctx.save();
ctx.shadowColor = ball.color;
ctx.shadowBlur = 22;
ctx.fillStyle = ball.color;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r * pulse, 0, TAU);
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(ball.x - 2, ball.y - 2, ball.r * 0.36, 0, TAU);
ctx.fill();
ctx.restore();
}
}
function drawPowerups() {
for (const p of powerups) {
const config = POWER[p.kind];
const cx = p.x + p.w / 2;
const cy = p.y + p.h / 2;
ctx.save();
ctx.globalAlpha = 0.18;
ctx.fillStyle = config.color;
ctx.beginPath();
ctx.arc(cx, cy - 9, 13, 0, TAU);
ctx.fill();
ctx.restore();
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(p.rotation);
ctx.shadowColor = config.color;
ctx.shadowBlur = 18;
roundRectPath(ctx, -14, -14, 28, 28, 7);
ctx.fillStyle = alphaColor(config.color, 0.18);
ctx.fill();
ctx.strokeStyle = config.color;
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = config.color;
ctx.font = "950 16px system-ui, sans-serif";
ctx.fillText(config.symbol, cx, cy + 0.5);
ctx.restore();
}
}
function drawParticles() {
for (const p of particles) {
const alpha = Math.max(0, p.life / p.maxLife);
ctx.save();
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
if (p.type === "confetti") {
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.fillRect(-p.size / 2, -p.size / 4, p.size, p.size / 2);
} else {
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * (0.4 + alpha * 0.6), 0, TAU);
ctx.fill();
}
ctx.restore();
}
for (const r of rings) {
ctx.save();
ctx.globalAlpha = Math.max(0, r.life / r.maxLife);
ctx.strokeStyle = r.color;
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.arc(r.x, r.y, r.radius, 0, TAU);
ctx.stroke();
ctx.restore();
}
}
function drawPopups() {
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "900 16px system-ui, sans-serif";
for (const p of popups) {
const alpha = Math.max(0, p.life / p.maxLife);
ctx.globalAlpha = alpha;
ctx.lineWidth = 4;
ctx.strokeStyle = "rgba(3,5,14,0.8)";
ctx.strokeText(p.text, p.x, p.y);
ctx.fillStyle = p.color;
ctx.fillText(p.text, p.x, p.y);
}
ctx.restore();
}
function drawCanvasHud() {
const accent = levels[levelIndex].accent;
ctx.save();
ctx.textBaseline = "middle";
ctx.fillStyle = "#7784a7";
ctx.font = "900 9px system-ui, sans-serif";
ctx.textAlign = "left";
ctx.fillText(`SECTOR ${String(levelIndex + 1).padStart(2, "0")} / ${String(levels.length).padStart(2, "0")}`, 24, 22);
ctx.fillStyle = accent;
ctx.font = "900 16px system-ui, sans-serif";
ctx.fillText(levels[levelIndex].name, 24, 45);
const progress = levelTotal ? 1 - destructibleLeft / levelTotal : 0;
const barX = W - 212;
const barY = 28;
ctx.fillStyle = "rgba(120,140,190,0.15)";
roundRectPath(ctx, barX, barY, 188, 7, 4);
ctx.fill();
ctx.fillStyle = accent;
roundRectPath(ctx, barX, barY, Math.max(4, 188 * progress), 7, 4);
ctx.fill();
ctx.fillStyle = "#8290b2";
ctx.font = "800 8px system-ui, sans-serif";
ctx.textAlign = "right";
ctx.fillText(`${Math.max(0, destructibleLeft)} TILES LEFT`, W - 24, 49);
if (comboTimer > 0 && combo > 0) {
const scale = 1 + Math.max(0, (comboTimer - 2.35)) * 0.35;
ctx.translate(W / 2, 32);
ctx.scale(scale, scale);
ctx.textAlign = "center";
ctx.fillStyle = "#ffd166";
ctx.shadowColor = "rgba(255,209,102,0.5)";
ctx.shadowBlur = 12;
ctx.font = "950 17px system-ui, sans-serif";
ctx.fillText(`CHAIN ${combo} ×${comboMultiplier()}`, 0, 0);
}
if (toast) {
const alpha = Math.min(1, toast.life * 2);
ctx.globalAlpha = alpha;
ctx.font = "950 12px system-ui, sans-serif";
const textWidth = ctx.measureText(toast.text).width + 28;
ctx.fillStyle = "rgba(7,10,24,0.88)";
roundRectPath(ctx, W / 2 - textWidth / 2, H - 43, textWidth, 25, 12);
ctx.fill();
ctx.strokeStyle = toast.color;
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = toast.color;
ctx.textAlign = "center";
ctx.fillText(toast.text, W / 2, H - 30);
}
ctx.restore();
}
function drawStateUI() {
if (levelBanner && state === "serve") {
ctx.save();
const alpha = Math.min(1, levelBanner.life / 0.4);
ctx.globalAlpha = alpha;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = levels[levelIndex].accent;
ctx.font = "950 29px system-ui, sans-serif";
ctx.fillText(levelBanner.title, W / 2, 323);
ctx.fillStyle = "#a9b5d0";
ctx.font = "700 12px system-ui, sans-serif";
ctx.fillText(levelBanner.subtitle, W / 2, 350);
ctx.restore();
}
if (state === "serve") {
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "rgba(6,9,23,0.76)";
roundRectPath(ctx, W / 2 - 186, 395, 372, 98, 15);
ctx.fill();
ctx.strokeStyle = "rgba(139,158,255,0.18)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = "#ffffff";
ctx.font = "950 25px system-ui, sans-serif";
ctx.fillText("READY?", W / 2, 425);
ctx.fillStyle = "#8f9cba";
ctx.font = "800 10px system-ui, sans-serif";
ctx.fillText("SPACE / CLICK TO LAUNCH", W / 2, 462);
ctx.restore();
}
if (state === "dying") {
ctx.save();
ctx.globalAlpha = Math.min(1, deathTimer * 1.4);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "#ff6fa7";
ctx.font = "950 30px system-ui, sans-serif";
ctx.fillText("BALL LOST", W / 2, H / 2 - 10);
ctx.fillStyle = "#9aa7c5";
ctx.font = "800 11px system-ui, sans-serif";
ctx.fillText(`${lives} LIVES REMAINING`, W / 2, H / 2 + 22);
ctx.restore();
}
}
function render() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, W, H);
drawBackground();
ctx.save();
if (shake > 0.05) {
ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
}
drawBricks();
drawPowerups();
drawPaddle();
drawBalls();
drawParticles();
drawPopups();
drawCanvasHud();
drawStateUI();
ctx.restore();
if (flash > 0.005) {
ctx.fillStyle = `rgba(255,255,255,${flash})`;
ctx.fillRect(0, 0, W, H);
}
}
let audioCtx = null;
let soundOn = true;
let lastBrickSound = 0;
function ensureAudio() {
try {
if (!audioCtx) {
const AudioCtor = window.AudioContext || window.webkitAudioContext;
if (AudioCtor) audioCtx = new AudioCtor();
}
if (audioCtx && audioCtx.state === "suspended") audioCtx.resume();
} catch (_) {}
}
function tone(freq, duration, type = "sine", volume = 0.04, slide = 0, delay = 0) {
if (!soundOn || !audioCtx) return;
const now = audioCtx.currentTime + delay;
const oscillator = audioCtx.createOscillator();
const gain = audioCtx.createGain();
oscillator.type = type;
oscillator.frequency.setValueAtTime(Math.max(25, freq), now);
if (slide) {
oscillator.frequency.exponentialRampToValueAtTime(Math.max(25, freq + slide), now + duration);
}
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(volume, now + 0.008);
gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);
oscillator.connect(gain);
gain.connect(audioCtx.destination);
oscillator.start(now);
oscillator.stop(now + duration + 0.02);
}
function sfx(name) {
if (!soundOn) return;
const now = performance.now();
if (name === "paddle") {
tone(180, 0.085, "sine", 0.045, 160);
} else if (name === "wall") {
tone(150, 0.035, "triangle", 0.018, 40);
} else if (name === "brick") {
if (now - lastBrickSound < 38) return;
lastBrickSound = now;
tone(360 + Math.random() * 100, 0.055, "square", 0.018, 120);
} else if (name === "steel") {
if (now - lastBrickSound < 55) return;
lastBrickSound = now;
tone(720, 0.07, "square", 0.022, -420);
} else if (name === "destroy") {
tone(430, 0.11, "triangle", 0.035, 310);
tone(620, 0.08, "sine", 0.022, 170, 0.035);
} else if (name === "power") {
tone(440, 0.11, "sine", 0.04, 190);
tone(660, 0.12, "sine", 0.035, 230, 0.08);
tone(880, 0.14, "sine", 0.03, 160, 0.16);
} else if (name === "launch") {
tone(190, 0.16, "triangle", 0.04, 470);
} else if (name === "lose") {
tone(260, 0.4, "sawtooth", 0.035, -190);
} else if (name === "clear") {
[440, 554, 659, 880].forEach((f, i) => tone(f, 0.18, "triangle", 0.035, 90, i * 0.09));
}
}
function toggleSound() {
soundOn = !soundOn;
if (soundOn) {
ensureAudio();
sfx("power");
}
els.sound.textContent = soundOn ? "SOUND ON" : "SOUND OFF";
els.sound.setAttribute("aria-pressed", String(soundOn));
}
function pointerPosition(event) {
const rect = canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * W / rect.width,
y: (event.clientY - rect.top) * H / rect.height
};
}
canvas.addEventListener("pointermove", event => {
if (state === "title") return;
const p = pointerPosition(event);
mouse.active = true;
mouse.center = Math.max(0, Math.min(W, p.x));
inputMode = "mouse";
});
canvas.addEventListener("pointerdown", event => {
if (event.pointerType === "mouse" && event.button !== 0) return;
event.preventDefault();
ensureAudio();
const p = pointerPosition(event);
mouse.active = true;
mouse.center = Math.max(0, Math.min(W, p.x));
inputMode = "mouse";
try { canvas.setPointerCapture(event.pointerId); } catch (_) {}
activatePrimary();
});
canvas.addEventListener("pointerup", event => {
try { canvas.releasePointerCapture(event.pointerId); } catch (_) {}
});
window.addEventListener("keydown", event => {
if (event.ctrlKey || event.metaKey || event.altKey) return;
if (["ArrowLeft", "ArrowRight", "ArrowDown", "ArrowUp", "Space"].includes(event.code)) {
event.preventDefault();
}
if (event.code === "ArrowLeft" || event.code === "ArrowRight" || event.code === "KeyA" || event.code === "KeyD") {
keys.add(event.code);
inputMode = "keyboard";
}
if (event.code === "Space" && !event.repeat) {
activatePrimary();
} else if (event.code === "KeyP" || event.code === "Escape") {
if (!event.repeat) togglePause();
} else if (event.code === "KeyR" && !event.repeat) {
startGame();
} else if (event.code === "KeyM" && !event.repeat) {
toggleSound();
}
});
window.addEventListener("keyup", event => {
keys.delete(event.code);
});
window.addEventListener("blur", () => {
keys.clear();
if (state === "playing" || state === "serve") togglePause();
});
document.addEventListener("visibilitychange", () => {
if (document.hidden && (state === "playing" || state === "serve")) togglePause();
});
els.overlayButton.addEventListener("click", event => {
event.stopPropagation();
ensureAudio();
if (overlayAction) overlayAction();
});
els.pause.addEventListener("click", togglePause);
els.sound.addEventListener("click", toggleSound);
els.reset.addEventListener("click", startGame);
window.addEventListener("resize", resizeCanvas);
let lastTime = performance.now();
function frame(now) {
const dt = Math.min(0.033, Math.max(0, (now - lastTime) / 1000));
lastTime = now;
if (hitStop > 0) {
hitStop = Math.max(0, hitStop - dt);
} else {
update(dt);
}
render();
requestAnimationFrame(frame);
}
resizeCanvas();
loadBricks(0);
updateHud();
showTitle();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

