1
2
3
4
5
6
7
8
9class Ball {
10 constructor(x = 400, y = 500, radius = 7, speed = 6) {
11 this.x = x;
12 this.y = y;
13 this.radius = radius;
14 this.baseSpeed = speed;
15 this.speed = speed;
16 this.vx = 0;
17 this.vy = -speed;
18 this.stuckToPaddle = true;
19 this.trail = [];
20 this.maxTrailLength = 10;
21 }
22
23 reset(paddleX, paddleY, paddleWidth) {
24 this.x = paddleX + paddleWidth / 2;
25 this.y = paddleY - this.radius - 2;
26 this.speed = this.baseSpeed;
27 this.vx = 0;
28 this.vy = -this.speed;
29 this.stuckToPaddle = true;
30 this.trail = [];
31 }
32
33 launch(angleOffset = 0) {
34 if (!this.stuckToPaddle) return;
35 this.stuckToPaddle = false;
36
37 const angle = (-Math.PI / 2) + angleOffset;
38 this.vx = this.speed * Math.cos(angle);
39 this.vy = this.speed * Math.sin(angle);
40 }
41
42 update(dt = 1) {
43 if (this.stuckToPaddle) return;
44
45
46 this.trail.unshift({ x: this.x, y: this.y });
47 if (this.trail.length > this.maxTrailLength) {
48 this.trail.pop();
49 }
50
51 this.x += this.vx * dt;
52 this.y += this.vy * dt;
53 }
54
55 setSpeed(newSpeed) {
56 const currentSpeed = Math.hypot(this.vx, this.vy) || 1;
57 this.speed = newSpeed;
58 this.vx = (this.vx / currentSpeed) * newSpeed;
59 this.vy = (this.vy / currentSpeed) * newSpeed;
60 }
61
62 getSpeed() {
63 return Math.hypot(this.vx, this.vy);
64 }
65}
66
67class Paddle {
68 constructor(canvasWidth = 800, canvasHeight = 600) {
69 this.width = 110;
70 this.height = 14;
71 this.x = (canvasWidth - this.width) / 2;
72 this.y = canvasHeight - 40;
73 this.speed = 9;
74 this.vx = 0;
− this.targetX = this.x;
75 }
76
77 moveLeft() {
78 this.vx = -this.speed;
79 }
80
81 moveRight() {
82 this.vx = this.speed;
83 }
84
85 stop() {
86 this.vx = 0;
87 }
88
89 update(canvasWidth) {
90 this.x += this.vx;
−
91 if (this.x < 0) {
92 this.x = 0;
93 } else if (this.x + this.width > canvasWidth) {
94 this.x = canvasWidth - this.width;
95 }
96 }
97
98 setPosition(x, canvasWidth) {
99 this.x = Math.max(0, Math.min(canvasWidth - this.width, x - this.width / 2));
100 }
101}
102
103class Brick {
104 constructor(id, x, y, width, height, hp = 1, color = '#00f3ff', scoreValue = 10) {
105 this.id = id;
106 this.x = x;
107 this.y = y;
108 this.width = width;
109 this.height = height;
110 this.hp = hp;
111 this.maxHp = hp;
112 this.color = color;
113 this.scoreValue = scoreValue;
114 this.active = true;
115 }
116
117 hit() {
118 if (!this.active) return false;
119 this.hp -= 1;
120 if (this.hp <= 0) {
121 this.active = false;
122 return true;
123 }
124 return false;
125 }
126}
127
128
129
130
131
132class PhysicsEngine {
133 static checkWallCollision(ball, canvasWidth, canvasHeight) {
134 let bounced = false;
135 let outOfBounds = false;
136
137
138 if (ball.x - ball.radius <= 0) {
139 ball.x = ball.radius;
140 ball.vx = Math.abs(ball.vx);
141 bounced = true;
142 }
143
144 else if (ball.x + ball.radius >= canvasWidth) {
145 ball.x = canvasWidth - ball.radius;
146 ball.vx = -Math.abs(ball.vx);
147 bounced = true;
148 }
149
150
151 if (ball.y - ball.radius <= 0) {
152 ball.y = ball.radius;
153 ball.vy = Math.abs(ball.vy);
154 bounced = true;
155 }
156
157
158 if (ball.y - ball.radius >= canvasHeight) {
159 outOfBounds = true;
160 }
161
162 return { bounced, outOfBounds };
163 }
164
165 static checkPaddleCollision(ball, paddle) {
166 if (ball.stuckToPaddle) return false;
167
−
168 const closestX = Math.max(paddle.x, Math.min(ball.x, paddle.x + paddle.width));
169 const closestY = Math.max(paddle.y, Math.min(ball.y, paddle.y + paddle.height));
170
171 const distX = ball.x - closestX;
172 const distY = ball.y - closestY;
173 const distanceSq = distX * distX + distY * distY;
174
175 if (distanceSq <= ball.radius * ball.radius) {
−
176 if (ball.vy > 0 && ball.y <= paddle.y + paddle.height / 2 + ball.radius) {
177 ball.y = paddle.y - ball.radius;
178
−
179 const paddleCenter = paddle.x + paddle.width / 2;
180 const hitOffset = (ball.x - paddleCenter) / (paddle.width / 2);
181 const clampedOffset = Math.max(-0.9, Math.min(0.9, hitOffset));
182
−
183 const maxAngle = Math.PI * 0.36;
184 const bounceAngle = clampedOffset * maxAngle;
185
186 const currentSpeed = ball.getSpeed();
187 ball.vx = currentSpeed * Math.sin(bounceAngle);
188 ball.vy = -currentSpeed * Math.cos(bounceAngle);
189
190 return true;
191 }
192 }
193 return false;
194 }
195
196 static checkBrickCollision(ball, brick) {
197 if (!brick.active || ball.stuckToPaddle) return null;
198
−
199 const closestX = Math.max(brick.x, Math.min(ball.x, brick.x + brick.width));
200 const closestY = Math.max(brick.y, Math.min(ball.y, brick.y + brick.height));
201
202 const distX = ball.x - closestX;
203 const distY = ball.y - closestY;
204 const distanceSq = distX * distX + distY * distY;
205
206 if (distanceSq < ball.radius * ball.radius) {
−
207 const prevX = ball.x - ball.vx;
208 const prevY = ball.y - ball.vy;
209
210 const hitFromLeft = prevX <= brick.x;
211 const hitFromRight = prevX >= brick.x + brick.width;
212 const hitFromTop = prevY <= brick.y;
213 const hitFromBottom = prevY >= brick.y + brick.height;
214
215 if (hitFromLeft || hitFromRight) {
216 ball.vx = hitFromLeft ? -Math.abs(ball.vx) : Math.abs(ball.vx);
217 }
218 if (hitFromTop || hitFromBottom) {
219 ball.vy = hitFromTop ? -Math.abs(ball.vy) : Math.abs(ball.vy);
220 }
221 if (!hitFromLeft && !hitFromRight && !hitFromTop && !hitFromBottom) {
−
222 ball.vy = -ball.vy;
223 }
224
225 const destroyed = brick.hit();
226 return { destroyed, hpRemaining: brick.hp };
227 }
228
229 return null;
230 }
231}
232
233
234
235
236
237class LevelManager {
238 static get COLOR_PALETTE() {
239 return [
240 { name: 'pink', color: '#ff007f', hp: 3, score: 50 },
241 { name: 'orange', color: '#ff6600', hp: 2, score: 30 },
242 { name: 'yellow', color: '#ffe600', hp: 1, score: 20 },
243 { name: 'green', color: '#00ff66', hp: 1, score: 15 },
244 { name: 'cyan', color: '#00f3ff', hp: 1, score: 10 }
245 ];
246 }
247
248 static createLevel(levelNumber, canvasWidth = 800) {
249 const cols = 9;
250 const rows = Math.min(8, 4 + Math.floor(levelNumber / 2));
251 const padding = 8;
− const marginTop = 70;
252 const marginTop = 65;
253 const marginLeft = 35;
254 const availableWidth = canvasWidth - marginLeft * 2;
255 const brickWidth = (availableWidth - (cols - 1) * padding) / cols;
256 const brickHeight = 22;
257
258 const bricks = [];
259 let idCounter = 1;
260
261 for (let r = 0; r < rows; r++) {
262 for (let c = 0; c < cols; c++) {
−
263 let shouldCreate = true;
264
265 if (levelNumber === 2) {
−
− if ((r + c) % 2 === 1 && r > 1 && r < rows - 1) shouldCreate = false;
266 if ((r + c) % 2 === 1 && r > 0 && r < rows - 1) shouldCreate = false;
267 } else if (levelNumber === 3) {
−
268 if (r === 2 && (c === 2 || c === 6)) shouldCreate = false;
269 } else if (levelNumber > 3) {
−
270 if (r % 2 === 1 && (c === 0 || c === cols - 1)) shouldCreate = false;
271 }
272
273 if (shouldCreate) {
274 const paletteIndex = r % this.COLOR_PALETTE.length;
275 const config = this.COLOR_PALETTE[paletteIndex];
276
−
277 let hp = config.hp;
278 if (levelNumber >= 3 && r === 0) hp = Math.min(4, hp + 1);
279
280 const x = marginLeft + c * (brickWidth + padding);
281 const y = marginTop + r * (brickHeight + padding);
282
283 bricks.push(new Brick(idCounter++, x, y, brickWidth, brickHeight, hp, config.color, config.score));
284 }
285 }
286 }
287
−
288 const baseSpeed = 6 + (levelNumber - 1) * 0.75;
−
289 return { bricks, baseSpeed };
290 }
291}
292
293
294
295
296
297class ParticleSystem {
298 constructor() {
299 this.particles = [];
300 }
301
302 spawnBrickExplosion(x, y, width, height, color, count = 14) {
303 for (let i = 0; i < count; i++) {
304 const px = x + Math.random() * width;
305 const py = y + Math.random() * height;
306 const angle = Math.random() * Math.PI * 2;
307 const speed = 1.5 + Math.random() * 4.5;
308 this.particles.push({
309 x: px,
310 y: py,
311 vx: Math.cos(angle) * speed,
312 vy: Math.sin(angle) * speed,
313 size: 2 + Math.random() * 4,
314 color: color,
315 alpha: 1,
316 decay: 0.02 + Math.random() * 0.03,
317 gravity: 0.1
318 });
319 }
320 }
321
322 spawnPaddleSpark(x, y, count = 8) {
323 for (let i = 0; i < count; i++) {
324 const angle = -Math.PI / 2 + (Math.random() - 0.5) * 1.2;
325 const speed = 2 + Math.random() * 4;
326 this.particles.push({
327 x: x,
328 y: y,
329 vx: Math.cos(angle) * speed,
330 vy: Math.sin(angle) * speed,
331 size: 2 + Math.random() * 3,
332 color: '#00f3ff',
333 alpha: 1,
334 decay: 0.04 + Math.random() * 0.04,
335 gravity: 0.05
336 });
337 }
338 }
339
340 update() {
341 for (let i = this.particles.length - 1; i >= 0; i--) {
342 const p = this.particles[i];
343 p.x += p.vx;
344 p.y += p.vy;
345 p.vy += p.gravity;
346 p.alpha -= p.decay;
347 if (p.alpha <= 0) {
348 this.particles.splice(i, 1);
349 }
350 }
351 }
352
353 draw(ctx) {
354 ctx.save();
355 for (const p of this.particles) {
356 ctx.globalAlpha = Math.max(0, p.alpha);
357 ctx.fillStyle = p.color;
358 ctx.shadowColor = p.color;
359 ctx.shadowBlur = 8;
360 ctx.beginPath();
361 ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
362 ctx.fill();
363 }
364 ctx.restore();
365 }
366
367 clear() {
368 this.particles = [];
369 }
370}
371
372
373
374
375
376class SoundSynth {
377 constructor() {
378 this.ctx = null;
379 this.muted = false;
380 if (typeof window !== 'undefined') {
381 const savedMute = localStorage.getItem('breakout_muted');
382 if (savedMute !== null) {
383 this.muted = savedMute === 'true';
384 }
385 }
386 }
387
388 init() {
389 if (!this.ctx && typeof window !== 'undefined') {
390 const AudioCtx = window.AudioContext || window.webkitAudioContext;
391 if (AudioCtx) {
392 this.ctx = new AudioCtx();
393 }
394 }
395 if (this.ctx && this.ctx.state === 'suspended') {
396 this.ctx.resume();
397 }
398 }
399
400 toggleMute() {
401 this.muted = !this.muted;
402 if (typeof window !== 'undefined') {
403 localStorage.setItem('breakout_muted', this.muted);
404 }
405 return this.muted;
406 }
407
408 playTone(freq, type, duration, startVol = 0.2, endVol = 0.001) {
409 if (this.muted) return;
410 this.init();
411 if (!this.ctx) return;
412
413 try {
414 const osc = this.ctx.createOscillator();
415 const gain = this.ctx.createGain();
416
417 osc.type = type;
418 osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
419
420 gain.gain.setValueAtTime(startVol, this.ctx.currentTime);
421 gain.gain.exponentialRampToValueAtTime(endVol, this.ctx.currentTime + duration);
422
423 osc.connect(gain);
424 gain.connect(this.ctx.destination);
425
426 osc.start();
427 osc.stop(this.ctx.currentTime + duration);
− } catch (e) {
−
− }
428 } catch (e) {}
429 }
430
431 playPaddleHit() {
432 if (this.muted) return;
433 this.init();
434 if (!this.ctx) return;
435 try {
436 const osc = this.ctx.createOscillator();
437 const gain = this.ctx.createGain();
438 osc.type = 'triangle';
439 osc.frequency.setValueAtTime(320, this.ctx.currentTime);
440 osc.frequency.exponentialRampToValueAtTime(540, this.ctx.currentTime + 0.08);
441
442 gain.gain.setValueAtTime(0.25, this.ctx.currentTime);
443 gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.08);
444
445 osc.connect(gain);
446 gain.connect(this.ctx.destination);
447
448 osc.start();
449 osc.stop(this.ctx.currentTime + 0.08);
450 } catch (e) {}
451 }
452
453 playWallHit() {
454 this.playTone(220, 'square', 0.05, 0.15);
455 }
456
457 playBrickHit() {
458 this.playTone(600, 'sine', 0.06, 0.2);
459 }
460
461 playBrickBreak() {
462 if (this.muted) return;
463 this.init();
464 if (!this.ctx) return;
465 try {
466 const osc = this.ctx.createOscillator();
467 const gain = this.ctx.createGain();
468 osc.type = 'sawtooth';
469 osc.frequency.setValueAtTime(800, this.ctx.currentTime);
470 osc.frequency.exponentialRampToValueAtTime(200, this.ctx.currentTime + 0.12);
471
472 gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
473 gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.12);
474
475 osc.connect(gain);
476 gain.connect(this.ctx.destination);
477
478 osc.start();
479 osc.stop(this.ctx.currentTime + 0.12);
480 } catch (e) {}
481 }
482
483 playLifeLost() {
484 if (this.muted) return;
485 this.init();
486 if (!this.ctx) return;
487 try {
488 const osc = this.ctx.createOscillator();
489 const gain = this.ctx.createGain();
490 osc.type = 'sawtooth';
491 osc.frequency.setValueAtTime(350, this.ctx.currentTime);
492 osc.frequency.linearRampToValueAtTime(100, this.ctx.currentTime + 0.35);
493
494 gain.gain.setValueAtTime(0.35, this.ctx.currentTime);
495 gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.35);
496
497 osc.connect(gain);
498 gain.connect(this.ctx.destination);
499
500 osc.start();
501 osc.stop(this.ctx.currentTime + 0.35);
502 } catch (e) {}
503 }
504
505 playLevelUp() {
506 if (this.muted) return;
507 const notes = [440, 554.37, 659.25, 880];
508 notes.forEach((freq, i) => {
509 setTimeout(() => {
510 this.playTone(freq, 'triangle', 0.15, 0.25);
511 }, i * 90);
512 });
513 }
514
515 playGameOver() {
516 if (this.muted) return;
517 const notes = [400, 350, 300, 220];
518 notes.forEach((freq, i) => {
519 setTimeout(() => {
520 this.playTone(freq, 'sawtooth', 0.25, 0.3);
521 }, i * 140);
522 });
523 }
524}
525
526
527
528
529
530const GameState = {
531 START: 'START',
532 PLAYING: 'PLAYING',
533 PAUSED: 'PAUSED',
534 LEVEL_COMPLETE: 'LEVEL_COMPLETE',
535 GAME_OVER: 'GAME_OVER'
536};
537
538class GameController {
539 constructor() {
540 this.score = 0;
541 this.highScore = this.loadHighScore();
542 this.lives = 3;
543 this.level = 1;
544 this.state = GameState.START;
545
546 this.canvasWidth = 800;
547 this.canvasHeight = 600;
548
549 this.paddle = new Paddle(this.canvasWidth, this.canvasHeight);
550 this.ball = new Ball(this.canvasWidth / 2, this.paddle.y - 10);
551 this.particles = new ParticleSystem();
552 this.sound = new SoundSynth();
553
554 this.bricks = [];
555 this.loadLevel(this.level);
556 }
557
558 loadHighScore() {
559 if (typeof window !== 'undefined' && window.localStorage) {
560 const val = parseInt(localStorage.getItem('breakout_highscore'), 10);
561 return isNaN(val) ? 0 : val;
562 }
563 return 0;
564 }
565
566 saveHighScore() {
567 if (this.score > this.highScore) {
568 this.highScore = this.score;
569 if (typeof window !== 'undefined' && window.localStorage) {
570 localStorage.setItem('breakout_highscore', this.highScore.toString());
571 }
− return true;
572 return true;
573 }
574 return false;
575 }
576
577 loadLevel(lvl) {
578 this.level = lvl;
579 const levelData = LevelManager.createLevel(this.level, this.canvasWidth);
580 this.bricks = levelData.bricks;
581 this.ball.baseSpeed = levelData.baseSpeed;
582 this.ball.reset(this.paddle.x, this.paddle.y, this.paddle.width);
583 }
584
585 startNewGame() {
586 this.score = 0;
587 this.lives = 3;
588 this.level = 1;
589 this.loadLevel(1);
590 this.state = GameState.PLAYING;
591 this.particles.clear();
592 }
593
594 launchBall() {
595 if (this.state === GameState.PLAYING && this.ball.stuckToPaddle) {
596 this.ball.launch((Math.random() - 0.5) * 0.2);
597 }
598 }
599
600 togglePause() {
601 if (this.state === GameState.PLAYING) {
602 this.state = GameState.PAUSED;
603 } else if (this.state === GameState.PAUSED) {
604 this.state = GameState.PLAYING;
605 }
606 }
607
608 update() {
609 if (this.state !== GameState.PLAYING) return;
610
611 this.paddle.update(this.canvasWidth);
612
613 if (this.ball.stuckToPaddle) {
614 this.ball.x = this.paddle.x + this.paddle.width / 2;
615 this.ball.y = this.paddle.y - this.ball.radius - 2;
616 } else {
617 this.ball.update();
618
−
619 const wallResult = PhysicsEngine.checkWallCollision(this.ball, this.canvasWidth, this.canvasHeight);
620 if (wallResult.bounced) {
621 this.sound.playWallHit();
622 }
623 if (wallResult.outOfBounds) {
624 this.lives -= 1;
625 this.sound.playLifeLost();
626
627 if (this.lives <= 0) {
628 this.state = GameState.GAME_OVER;
629 this.saveHighScore();
630 this.sound.playGameOver();
631 } else {
632 this.ball.reset(this.paddle.x, this.paddle.y, this.paddle.width);
633 }
634 return;
635 }
636
−
637 if (PhysicsEngine.checkPaddleCollision(this.ball, this.paddle)) {
638 this.sound.playPaddleHit();
639 this.particles.spawnPaddleSpark(this.ball.x, this.paddle.y);
640 }
641
−
642 let remainingActive = 0;
643 for (const brick of this.bricks) {
644 if (brick.active) {
645 const hitResult = PhysicsEngine.checkBrickCollision(this.ball, brick);
646 if (hitResult) {
647 if (hitResult.destroyed) {
648 this.score += brick.scoreValue;
649 this.saveHighScore();
650 this.sound.playBrickBreak();
651 this.particles.spawnBrickExplosion(brick.x, brick.y, brick.width, brick.height, brick.color);
652 } else {
653 this.sound.playBrickHit();
654 }
655 }
656 if (brick.active) {
657 remainingActive++;
658 }
659 }
660 }
661
−
662 if (remainingActive === 0) {
663 this.state = GameState.LEVEL_COMPLETE;
664 this.sound.playLevelUp();
665 setTimeout(() => {
666 if (this.state === GameState.LEVEL_COMPLETE) {
667 this.loadLevel(this.level + 1);
668 this.state = GameState.PLAYING;
669 }
670 }, 1800);
671 }
672 }
673
674 this.particles.update();
675 }
676}
677
678
679
680
681
682if (typeof window !== 'undefined' && typeof document !== 'undefined') {
683 window.addEventListener('DOMContentLoaded', () => {
684 const canvas = document.getElementById('gameCanvas');
685 if (!canvas) return;
686
687 const ctx = canvas.getContext('2d');
688 const game = new GameController();
689
690
691 const scoreVal = document.getElementById('scoreVal');
692 const highScoreVal = document.getElementById('highScoreVal');
693 const levelVal = document.getElementById('levelVal');
694 const livesContainer = document.getElementById('livesContainer');
695 const muteBtn = document.getElementById('muteBtn');
696
697
698 const startOverlay = document.getElementById('startOverlay');
699 const pauseOverlay = document.getElementById('pauseOverlay');
700 const gameOverOverlay = document.getElementById('gameOverOverlay');
701 const levelOverlay = document.getElementById('levelOverlay');
702
703 const finalScore = document.getElementById('finalScore');
704 const finalHighScore = document.getElementById('finalHighScore');
705 const newRecordBadge = document.getElementById('newRecordBadge');
706 const levelBanner = document.getElementById('levelBanner');
707
708 const startBtn = document.getElementById('startBtn');
709 const restartBtn = document.getElementById('restartBtn');
710 const resumeBtn = document.getElementById('resumeBtn');
711
712
713 highScoreVal.textContent = game.highScore;
714
715 function updateMuteBtnUI() {
716 muteBtn.innerHTML = game.sound.muted ? '🔇 Muted' : '🔊 Sound';
717 muteBtn.classList.toggle('muted', game.sound.muted);
718 }
719 updateMuteBtnUI();
720
721 muteBtn.addEventListener('click', (e) => {
722 e.blur();
723 game.sound.toggleMute();
724 updateMuteBtnUI();
725 });
726
727
728 const keys = {};
729
730 window.addEventListener('keydown', (e) => {
731 keys[e.code] = true;
732
733 if (e.code === 'Space') {
734 e.preventDefault();
735 if (game.state === GameState.START) {
736 game.startNewGame();
737 } else if (game.state === GameState.GAME_OVER) {
738 game.startNewGame();
739 } else if (game.state === GameState.PLAYING) {
740 game.launchBall();
741 }
742 } else if (e.code === 'KeyP' || e.code === 'Escape') {
743 e.preventDefault();
744 game.togglePause();
745 } else if (e.code === 'KeyM') {
746 game.sound.toggleMute();
747 updateMuteBtnUI();
748 }
749 });
750
751 window.addEventListener('keyup', (e) => {
752 keys[e.code] = false;
753 });
754
755
756 canvas.addEventListener('mousemove', (e) => {
757 const rect = canvas.getBoundingClientRect();
758 const scaleX = canvas.width / rect.width;
759 const mouseX = (e.clientX - rect.left) * scaleX;
760 game.paddle.setPosition(mouseX, game.canvasWidth);
761 });
762
763 canvas.addEventListener('click', () => {
764 game.sound.init();
765 if (game.state === GameState.START) {
766 game.startNewGame();
767 } else if (game.state === GameState.PLAYING) {
768 game.launchBall();
769 }
770 });
771
772
773 canvas.addEventListener('touchmove', (e) => {
774 e.preventDefault();
775 if (e.touches.length > 0) {
776 const rect = canvas.getBoundingClientRect();
777 const scaleX = canvas.width / rect.width;
778 const touchX = (e.touches[0].clientX - rect.left) * scaleX;
779 game.paddle.setPosition(touchX, game.canvasWidth);
780 }
781 }, { passive: false });
782
783 canvas.addEventListener('touchstart', (e) => {
784 game.sound.init();
785 if (game.state === GameState.START) {
786 game.startNewGame();
787 } else if (game.state === GameState.PLAYING) {
788 game.launchBall();
789 }
790 });
791
792 if (startBtn) startBtn.addEventListener('click', () => game.startNewGame());
793 if (restartBtn) restartBtn.addEventListener('click', () => game.startNewGame());
794 if (resumeBtn) resumeBtn.addEventListener('click', () => game.togglePause());
795
796
797 function handleKeyboardInput() {
798 if (keys['ArrowLeft'] || keys['KeyA']) {
799 game.paddle.moveLeft();
800 } else if (keys['ArrowRight'] || keys['KeyD']) {
801 game.paddle.moveRight();
802 } else {
803 game.paddle.stop();
804 }
805 }
806
807
808 function drawBackground() {
809 ctx.fillStyle = '#08091a';
810 ctx.fillRect(0, 0, canvas.width, canvas.height);
811
812
813 ctx.strokeStyle = 'rgba(0, 243, 255, 0.03)';
814 ctx.lineWidth = 1;
815 const gridSize = 40;
816 for (let x = 0; x < canvas.width; x += gridSize) {
817 ctx.beginPath();
818 ctx.moveTo(x, 0);
819 ctx.lineTo(x, canvas.height);
820 ctx.stroke();
821 }
822 for (let y = 0; y < canvas.height; y += gridSize) {
823 ctx.beginPath();
824 ctx.moveTo(0, y);
825 ctx.lineTo(canvas.width, y);
826 ctx.stroke();
827 }
828 }
829
830 function drawBricks() {
831 for (const brick of game.bricks) {
832 if (!brick.active) continue;
833
834 ctx.save();
835
836 const grad = ctx.createLinearGradient(brick.x, brick.y, brick.x, brick.y + brick.height);
837 grad.addColorStop(0, brick.color);
838 grad.addColorStop(1, '#0c0d24');
839
840 ctx.fillStyle = grad;
841 ctx.shadowColor = brick.color;
842 ctx.shadowBlur = 10;
843
844
845 const r = 4;
846 ctx.beginPath();
847 ctx.roundRect(brick.x, brick.y, brick.width, brick.height, r);
848 ctx.fill();
849
850
851 ctx.lineWidth = 1.5;
852 ctx.strokeStyle = brick.color;
853 ctx.stroke();
854
855
856 ctx.beginPath();
857 ctx.strokeStyle = 'rgba(255, 255, 255, 0.35)';
858 ctx.lineWidth = 1;
859 ctx.moveTo(brick.x + r, brick.y + 2);
860 ctx.lineTo(brick.x + brick.width - r, brick.y + 2);
861 ctx.stroke();
862
863
864 if (brick.maxHp > 1) {
865 if (brick.hp < brick.maxHp) {
866 ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
867 ctx.beginPath();
868 ctx.moveTo(brick.x + 8, brick.y + brick.height / 2);
869 ctx.lineTo(brick.x + brick.width - 8, brick.y + brick.height / 2);
870 ctx.stroke();
871 }
872 }
873
874 ctx.restore();
875 }
876 }
877
878 function drawPaddle() {
879 const p = game.paddle;
880 ctx.save();
881
882
883 const grad = ctx.createLinearGradient(p.x, p.y, p.x, p.y + p.height);
884 grad.addColorStop(0, '#00f3ff');
885 grad.addColorStop(0.5, '#b500ff');
886 grad.addColorStop(1, '#00f3ff');
887
888 ctx.fillStyle = grad;
889 ctx.shadowColor = '#00f3ff';
890 ctx.shadowBlur = 14;
891
892 ctx.beginPath();
893 ctx.roundRect(p.x, p.y, p.width, p.height, 7);
894 ctx.fill();
895
896
897 ctx.strokeStyle = '#ffffff';
898 ctx.lineWidth = 1.5;
899 ctx.beginPath();
900 ctx.roundRect(p.x + 2, p.y + 1, p.width - 4, 3, 2);
901 ctx.stroke();
902
903 ctx.restore();
904 }
905
906 function drawBall() {
907 const b = game.ball;
908 ctx.save();
909
910
911 for (let i = 0; i < b.trail.length; i++) {
912 const pt = b.trail[i];
913 const alpha = (1 - i / b.trail.length) * 0.45;
914 const radius = b.radius * (1 - (i / b.trail.length) * 0.5);
915
916 ctx.fillStyle = '#00f3ff';
917 ctx.globalAlpha = alpha;
918 ctx.shadowColor = '#ff007f';
919 ctx.shadowBlur = 8;
920 ctx.beginPath();
921 ctx.arc(pt.x, pt.y, radius, 0, Math.PI * 2);
922 ctx.fill();
923 }
924
925
926 ctx.globalAlpha = 1.0;
927 ctx.fillStyle = '#ffffff';
928 ctx.shadowColor = '#00f3ff';
929 ctx.shadowBlur = 15;
930
931 ctx.beginPath();
932 ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
933 ctx.fill();
934
935
936 ctx.strokeStyle = '#00f3ff';
937 ctx.lineWidth = 1.5;
938 ctx.stroke();
939
940 ctx.restore();
941 }
942
943 function updateUI() {
944 scoreVal.textContent = game.score;
945 highScoreVal.textContent = game.highScore;
946 levelVal.textContent = game.level;
947
948
949 let livesHTML = '';
950 for (let i = 0; i < 3; i++) {
951 const active = i < game.lives ? 'active' : 'lost';
952 livesHTML += `<span class="life-icon ${active}">⚡</span>`;
953 }
954 livesContainer.innerHTML = livesHTML;
955
956
957 startOverlay.classList.toggle('hidden', game.state !== GameState.START);
958 pauseOverlay.classList.toggle('hidden', game.state !== GameState.PAUSED);
959 gameOverOverlay.classList.toggle('hidden', game.state !== GameState.GAME_OVER);
960 levelOverlay.classList.toggle('hidden', game.state !== GameState.LEVEL_COMPLETE);
961
962 if (game.state === GameState.GAME_OVER) {
963 finalScore.textContent = game.score;
964 finalHighScore.textContent = game.highScore;
965 const isNewRecord = game.score > 0 && game.score >= game.highScore;
966 newRecordBadge.classList.toggle('hidden', !isNewRecord);
967 } else if (game.state === GameState.LEVEL_COMPLETE) {
968 levelBanner.textContent = `LEVEL ${game.level + 1}`;
969 }
970 }
971
972
973 function gameLoop() {
974 handleKeyboardInput();
975 game.update();
976
977
978 drawBackground();
979 drawBricks();
980 drawPaddle();
981 drawBall();
982 game.particles.draw(ctx);
983 updateUI();
984
985 requestAnimationFrame(gameLoop);
986 }
987
988
989 requestAnimationFrame(gameLoop);
990 });
991}
992
993
994if (typeof module !== 'undefined' && module.exports) {
995 module.exports = {
996 Ball,
997 Paddle,
998 Brick,
999 PhysicsEngine,
1000 LevelManager,
1001 ParticleSystem,
1002 SoundSynth,
1003 GameState,
1004 GameController
1005 };
1006}
1007
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.