1(function (root, factory) {
2 const api = factory();
3 if (typeof module !== "undefined" && module.exports) module.exports = api;
4 root.BreakoutCore = api;
5 if (typeof window !== "undefined") window.addEventListener("DOMContentLoaded", () => api.boot());
6})(typeof globalThis !== "undefined" ? globalThis : this, function () {
7 "use strict";
8
9 const W = 900, H = 600;
10 const COLORS = ["#ff2bd6", "#28f7ff", "#ffe66d", "#5cff89", "#a855ff"];
11
12 function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
13
14 function circleRectCollision(ball, rect) {
15 const cx = clamp(ball.x, rect.x, rect.x + rect.w);
16 const cy = clamp(ball.y, rect.y, rect.y + rect.h);
17 const dx = ball.x - cx, dy = ball.y - cy;
18 return dx * dx + dy * dy <= ball.r * ball.r;
19 }
20
21 function createState(level = 1) {
22 const state = {
23 width: W,
24 height: H,
25 score: 0,
26 highScore: 0,
27 lives: 3,
28 level,
29 paused: true,
30 gameOver: false,
31 won: false,
32 keys: {},
33 particles: [],
34 paddle: { x: W / 2 - 65, y: H - 48, w: 130, h: 16, speed: 560 },
35 ball: { x: W / 2, y: H - 70, r: 8, vx: 0, vy: 0, stuck: true, speed: 330 + (level - 1) * 38 },
36 bricks: []
37 };
38 buildBricks(state);
39 return state;
40 }
41
42 function buildBricks(state) {
43 state.bricks.length = 0;
44 const rows = Math.min(5 + state.level, 9), cols = 10, gap = 8;
45 const bw = (state.width - 90 - gap * (cols - 1)) / cols, bh = 24;
46 for (let r = 0; r < rows; r++) {
47 for (let c = 0; c < cols; c++) {
48 state.bricks.push({
49 x: 45 + c * (bw + gap), y: 68 + r * (bh + gap), w: bw, h: bh,
50 alive: true, color: COLORS[r % COLORS.length], points: (rows - r) * 10
51 });
52 }
53 }
54 }
55
56 function resetBall(state) {
57 state.ball.x = state.paddle.x + state.paddle.w / 2;
58 state.ball.y = state.paddle.y - state.ball.r - 2;
59 state.ball.vx = 0;
60 state.ball.vy = 0;
61 state.ball.stuck = true;
62 }
63
64 function launchBall(state) {
65 if (!state.ball.stuck) return;
66 const dir = Math.random() < 0.5 ? -1 : 1;
67 state.ball.vx = dir * state.ball.speed * 0.46;
68 state.ball.vy = -state.ball.speed;
69 state.ball.stuck = false;
70 }
71
72 function reflectFromPaddle(ball, paddle) {
73 const hit = ((ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2));
74 const angle = clamp(hit, -1, 1) * (Math.PI * 0.38);
75 const speed = Math.hypot(ball.vx, ball.vy) || ball.speed;
76 ball.vx = Math.sin(angle) * speed;
77 ball.vy = -Math.abs(Math.cos(angle) * speed);
78 ball.y = paddle.y - ball.r - 0.1;
79 }
80
81 function collideBrick(ball, brick) {
82 const prevX = ball.x - ball.vx * 0.016;
83 const prevY = ball.y - ball.vy * 0.016;
84 const wasAbove = prevY <= brick.y, wasBelow = prevY >= brick.y + brick.h;
85 const wasLeft = prevX <= brick.x, wasRight = prevX >= brick.x + brick.w;
86 if (wasAbove || wasBelow) ball.vy *= -1;
87 else if (wasLeft || wasRight) ball.vx *= -1;
88 else ball.vy *= -1;
89 }
90
91 function spawnParticles(state, x, y, color) {
92 for (let i = 0; i < 14; i++) {
93 const a = Math.random() * Math.PI * 2, s = 70 + Math.random() * 190;
94 state.particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, life: 0.45 + Math.random() * 0.35, color });
95 }
96 }
97
98 function step(state, dt, input, hooks) {
99 hooks = hooks || {};
100 if (state.paused || state.gameOver) return state;
101 dt = Math.min(dt, 0.033);
102 const p = state.paddle, b = state.ball;
103 const move = (input && input.move) || 0;
104 p.x = clamp(p.x + move * p.speed * dt, 0, state.width - p.w);
105 if (input && typeof input.pointerX === "number") p.x = clamp(input.pointerX - p.w / 2, 0, state.width - p.w);
106 if (b.stuck) { b.x = p.x + p.w / 2; b.y = p.y - b.r - 2; }
107 else {
108 b.x += b.vx * dt; b.y += b.vy * dt;
109 if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); hooks.sound && hooks.sound("wall"); }
110 if (b.x + b.r > state.width) { b.x = state.width - b.r; b.vx = -Math.abs(b.vx); hooks.sound && hooks.sound("wall"); }
111 if (b.y - b.r < 0) { b.y = b.r; b.vy = Math.abs(b.vy); hooks.sound && hooks.sound("wall"); }
112 if (circleRectCollision(b, p) && b.vy > 0) { reflectFromPaddle(b, p); hooks.sound && hooks.sound("paddle"); }
113 for (const brick of state.bricks) {
114 if (brick.alive && circleRectCollision(b, brick)) {
115 brick.alive = false; state.score += brick.points; collideBrick(b, brick);
116 spawnParticles(state, b.x, b.y, brick.color); hooks.sound && hooks.sound("brick");
117 break;
118 }
119 }
120 if (b.y - b.r > state.height) {
121 state.lives--; hooks.sound && hooks.sound("lose");
122 if (state.lives <= 0) state.gameOver = true; else resetBall(state);
123 }
124 if (state.bricks.every(br => !br.alive)) {
125 state.level++; state.ball.speed += 38; buildBricks(state); resetBall(state); state.paused = true; state.won = true; hooks.sound && hooks.sound("level");
126 }
127 }
128 for (const part of state.particles) { part.x += part.vx * dt; part.y += part.vy * dt; part.vy += 280 * dt; part.life -= dt; }
129 state.particles = state.particles.filter(part => part.life > 0);
130 return state;
131 }
132
133 function boot() {
134 const canvas = document.getElementById("game"), ctx = canvas.getContext("2d");
135 const ui = {
136 score: document.getElementById("score"), high: document.getElementById("highScore"), lives: document.getElementById("lives"), level: document.getElementById("level"),
137 overlay: document.getElementById("overlay"), start: document.getElementById("startBtn"), pause: document.getElementById("pauseBtn"), mute: document.getElementById("muteBtn")
138 };
139 const state = createState();
140 state.highScore = Number(localStorage.getItem("neonBreakoutHigh") || 0);
141 let last = performance.now(), pointerX = null, muted = false, audioCtx = null;
142
143 function beep(kind) {
144 if (muted) return;
145 audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
146 const o = audioCtx.createOscillator(), g = audioCtx.createGain();
147 const table = { wall: 180, paddle: 320, brick: 620, lose: 90, level: 880 };
148 o.type = kind === "brick" ? "square" : "sine"; o.frequency.value = table[kind] || 240;
149 g.gain.setValueAtTime(0.045, audioCtx.currentTime); g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.11);
150 o.connect(g).connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime + 0.12);
151 }
152 function setOverlay(title, text) {
153 ui.overlay.querySelector("h2").textContent = title;
154 ui.overlay.querySelector("p").textContent = text;
155 ui.overlay.classList.remove("hidden");
156 }
157 function start() { if (state.gameOver) Object.assign(state, createState()); state.paused = false; state.won = false; ui.overlay.classList.add("hidden"); if (state.ball.stuck) launchBall(state); }
158 ui.start.onclick = start;
159 ui.pause.onclick = () => togglePause();
160 ui.mute.onclick = () => { muted = !muted; ui.mute.textContent = muted ? "Sound Off" : "Sound On"; ui.mute.setAttribute("aria-pressed", String(muted)); };
161 function togglePause() { state.paused = !state.paused; state.paused ? setOverlay("PAUSED", "Press P or Pause to resume") : ui.overlay.classList.add("hidden"); }
162 addEventListener("keydown", e => { if (["ArrowLeft","ArrowRight"," "].includes(e.key)) e.preventDefault(); state.keys[e.key.toLowerCase()] = true; if (e.key === " ") start(); if (e.key.toLowerCase() === "p") togglePause(); });
163 addEventListener("keyup", e => state.keys[e.key.toLowerCase()] = false);
164 canvas.addEventListener("mousemove", e => { const r = canvas.getBoundingClientRect(); pointerX = (e.clientX - r.left) * canvas.width / r.width; });
165 canvas.addEventListener("mouseleave", () => pointerX = null);
166 canvas.addEventListener("click", start);
167
168 function draw() {
169 ctx.clearRect(0, 0, W, H);
170 ctx.fillStyle = "#050510"; ctx.fillRect(0, 0, W, H);
171 ctx.strokeStyle = "rgba(40,247,255,.18)"; ctx.lineWidth = 1;
172 for (let y = 0; y < H; y += 30) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }
173 for (const br of state.bricks) if (br.alive) { ctx.fillStyle = br.color; ctx.shadowColor = br.color; ctx.shadowBlur = 14; roundRect(br.x, br.y, br.w, br.h, 6); ctx.fill(); }
174 ctx.shadowBlur = 18; ctx.shadowColor = "#28f7ff"; ctx.fillStyle = "#28f7ff"; roundRect(state.paddle.x, state.paddle.y, state.paddle.w, state.paddle.h, 8); ctx.fill();
175 ctx.shadowColor = "#ffe66d"; ctx.beginPath(); ctx.arc(state.ball.x, state.ball.y, state.ball.r, 0, Math.PI * 2); ctx.fillStyle = "#fff7a8"; ctx.fill();
176 for (const part of state.particles) { ctx.globalAlpha = Math.max(0, part.life * 2); ctx.fillStyle = part.color; ctx.fillRect(part.x, part.y, 4, 4); }
177 ctx.globalAlpha = 1; ctx.shadowBlur = 0;
178 }
179 function roundRect(x, y, w, h, r) { ctx.beginPath(); ctx.roundRect ? ctx.roundRect(x, y, w, h, r) : ctx.rect(x, y, w, h); }
180 function frame(now) {
181 const move = (state.keys.arrowright || state.keys.d ? 1 : 0) - (state.keys.arrowleft || state.keys.a ? 1 : 0);
182 step(state, (now - last) / 1000, { move, pointerX }, { sound: beep }); last = now;
183 state.highScore = Math.max(state.highScore, state.score); localStorage.setItem("neonBreakoutHigh", state.highScore);
184 ui.score.textContent = state.score; ui.high.textContent = state.highScore; ui.lives.textContent = state.lives; ui.level.textContent = state.level;
185 if (state.gameOver) setOverlay("GAME OVER", "Click INSERT COIN or press Space to play again");
186 else if (state.won && state.paused) setOverlay("LEVEL " + state.level, "Speed increased. Press Space to launch.");
187 draw(); requestAnimationFrame(frame);
188 }
189 setOverlay("READY?", "Move: ← → / A D / mouse · Launch: Space · Pause: P"); requestAnimationFrame(frame);
190 }
191
192 return { W, H, clamp, circleRectCollision, createState, buildBricks, resetBall, launchBall, reflectFromPaddle, collideBrick, step, boot };
193});
194
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.