astrabot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +411 -0
  2. package/ai/ai.config.ts +27 -0
  3. package/ai/auto-retry.ts +117 -0
  4. package/ai/config-loader.ts +132 -0
  5. package/ai/index.ts +4 -0
  6. package/ai/retry-prompt.ts +30 -0
  7. package/bin/astra +2 -0
  8. package/core/retry/error-classifier.ts +208 -0
  9. package/core/retry/index.ts +29 -0
  10. package/core/retry/retry-config.ts +142 -0
  11. package/core/retry/retry-engine.ts +215 -0
  12. package/game/index.html +573 -0
  13. package/game/neon-breaker.html +1037 -0
  14. package/index.ts +140 -0
  15. package/modes/agent/action-tracker.ts +47 -0
  16. package/modes/agent/agent-tools.ts +338 -0
  17. package/modes/agent/approval.ts +184 -0
  18. package/modes/agent/diff-view.ts +34 -0
  19. package/modes/agent/orchestrator.ts +234 -0
  20. package/modes/agent/tool-executor.ts +993 -0
  21. package/modes/agent/types.ts +68 -0
  22. package/modes/ask/orchestrator.ts +230 -0
  23. package/modes/auto.ts +88 -0
  24. package/modes/cli.ts +43 -0
  25. package/modes/multi/agent-pool-manager.ts +337 -0
  26. package/modes/multi/examples.ts +441 -0
  27. package/modes/multi/message-broker.ts +179 -0
  28. package/modes/multi/multi-agent-orchestrator.ts +891 -0
  29. package/modes/multi/orchestrator.ts +414 -0
  30. package/modes/multi/types.ts +245 -0
  31. package/modes/multi/workflow-builder.ts +569 -0
  32. package/modes/plan/orchestrator.ts +198 -0
  33. package/modes/plan/planner.ts +121 -0
  34. package/modes/plan/selection.ts +43 -0
  35. package/modes/plan/types.ts +13 -0
  36. package/modes/plan/web-tools.ts +132 -0
  37. package/modes/setup.ts +210 -0
  38. package/package.json +62 -0
  39. package/session/index.ts +45 -0
  40. package/session/session-context.ts +188 -0
  41. package/session/session-manager.ts +374 -0
  42. package/session/session-tools.ts +109 -0
  43. package/session/store.ts +278 -0
  44. package/tsconfig.json +30 -0
  45. package/tui/spinner.ts +182 -0
  46. package/tui/terminal-md.ts +17 -0
  47. package/tui/wakeup.ts +231 -0
@@ -0,0 +1,1037 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Neon Breaker</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ display: flex;
16
+ justify-content: center;
17
+ align-items: center;
18
+ min-height: 100vh;
19
+ background: #0a0a0f;
20
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
21
+ overflow: hidden;
22
+ }
23
+
24
+ .game-wrapper {
25
+ position: relative;
26
+ display: flex;
27
+ flex-direction: column;
28
+ align-items: center;
29
+ }
30
+
31
+ .header {
32
+ display: flex;
33
+ justify-content: space-between;
34
+ align-items: center;
35
+ width: 480px;
36
+ padding: 15px 20px;
37
+ background: linear-gradient(180deg, rgba(20, 20, 35, 0.95), rgba(10, 10, 20, 0.9));
38
+ border-radius: 15px 15px 0 0;
39
+ border: 1px solid rgba(0, 255, 255, 0.2);
40
+ border-bottom: none;
41
+ }
42
+
43
+ .header h1 {
44
+ font-size: 1.5rem;
45
+ background: linear-gradient(90deg, #00ffff, #ff00ff, #ffff00);
46
+ -webkit-background-clip: text;
47
+ -webkit-text-fill-color: transparent;
48
+ background-clip: text;
49
+ text-shadow: none;
50
+ letter-spacing: 2px;
51
+ }
52
+
53
+ .stats {
54
+ display: flex;
55
+ gap: 20px;
56
+ }
57
+
58
+ .stat {
59
+ text-align: center;
60
+ }
61
+
62
+ .stat-label {
63
+ font-size: 0.7rem;
64
+ color: rgba(255, 255, 255, 0.5);
65
+ text-transform: uppercase;
66
+ letter-spacing: 1px;
67
+ }
68
+
69
+ .stat-value {
70
+ font-size: 1.2rem;
71
+ font-weight: bold;
72
+ color: #00ffff;
73
+ text-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
74
+ }
75
+
76
+ .stat-value.lives {
77
+ color: #ff4444;
78
+ text-shadow: 0 0 10px rgba(255, 68, 68, 0.5);
79
+ }
80
+
81
+ .stat-value.combo {
82
+ color: #ffff00;
83
+ text-shadow: 0 0 10px rgba(255, 255, 0, 0.5);
84
+ }
85
+
86
+ #gameCanvas {
87
+ display: block;
88
+ border-left: 1px solid rgba(0, 255, 255, 0.2);
89
+ border-right: 1px solid rgba(0, 255, 255, 0.2);
90
+ background: #050510;
91
+ }
92
+
93
+ .footer {
94
+ width: 480px;
95
+ padding: 12px 20px;
96
+ background: linear-gradient(0deg, rgba(20, 20, 35, 0.95), rgba(10, 10, 20, 0.9));
97
+ border-radius: 0 0 15px 15px;
98
+ border: 1px solid rgba(0, 255, 255, 0.2);
99
+ border-top: none;
100
+ display: flex;
101
+ justify-content: center;
102
+ gap: 10px;
103
+ }
104
+
105
+ .btn {
106
+ padding: 10px 25px;
107
+ font-size: 0.9rem;
108
+ font-weight: bold;
109
+ border: none;
110
+ border-radius: 25px;
111
+ cursor: pointer;
112
+ transition: all 0.3s ease;
113
+ text-transform: uppercase;
114
+ letter-spacing: 1px;
115
+ }
116
+
117
+ .btn-primary {
118
+ background: linear-gradient(135deg, #00ffff, #0088ff);
119
+ color: #000;
120
+ box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
121
+ }
122
+
123
+ .btn-primary:hover {
124
+ transform: scale(1.05);
125
+ box-shadow: 0 0 30px rgba(0, 255, 255, 0.5);
126
+ }
127
+
128
+ .btn-secondary {
129
+ background: transparent;
130
+ border: 2px solid rgba(255, 0, 255, 0.5);
131
+ color: #ff00ff;
132
+ }
133
+
134
+ .btn-secondary:hover {
135
+ background: rgba(255, 0, 255, 0.1);
136
+ border-color: #ff00ff;
137
+ }
138
+
139
+ .overlay {
140
+ position: absolute;
141
+ top: 0;
142
+ left: 0;
143
+ width: 100%;
144
+ height: 100%;
145
+ background: rgba(0, 0, 0, 0.85);
146
+ display: flex;
147
+ flex-direction: column;
148
+ justify-content: center;
149
+ align-items: center;
150
+ z-index: 10;
151
+ backdrop-filter: blur(5px);
152
+ }
153
+
154
+ .overlay.hidden {
155
+ display: none;
156
+ }
157
+
158
+ .overlay h2 {
159
+ font-size: 3rem;
160
+ margin-bottom: 10px;
161
+ background: linear-gradient(90deg, #00ffff, #ff00ff);
162
+ -webkit-background-clip: text;
163
+ -webkit-text-fill-color: transparent;
164
+ background-clip: text;
165
+ }
166
+
167
+ .overlay .subtitle {
168
+ color: rgba(255, 255, 255, 0.7);
169
+ margin-bottom: 30px;
170
+ font-size: 1.1rem;
171
+ }
172
+
173
+ .overlay .final-score {
174
+ font-size: 2rem;
175
+ color: #ffff00;
176
+ margin-bottom: 20px;
177
+ text-shadow: 0 0 20px rgba(255, 255, 0, 0.5);
178
+ }
179
+
180
+ .overlay .high-score {
181
+ font-size: 1rem;
182
+ color: rgba(255, 255, 255, 0.5);
183
+ margin-bottom: 30px;
184
+ }
185
+
186
+ .powerup-indicator {
187
+ position: absolute;
188
+ bottom: 80px;
189
+ left: 50%;
190
+ transform: translateX(-50%);
191
+ display: flex;
192
+ gap: 10px;
193
+ z-index: 5;
194
+ }
195
+
196
+ .powerup-badge {
197
+ padding: 5px 15px;
198
+ border-radius: 15px;
199
+ font-size: 0.8rem;
200
+ font-weight: bold;
201
+ animation: pulse 1s infinite;
202
+ }
203
+
204
+ @keyframes pulse {
205
+ 0%, 100% { opacity: 1; }
206
+ 50% { opacity: 0.7; }
207
+ }
208
+
209
+ .instructions {
210
+ color: rgba(255, 255, 255, 0.4);
211
+ font-size: 0.8rem;
212
+ margin-top: 15px;
213
+ }
214
+
215
+ .instructions kbd {
216
+ background: rgba(255, 255, 255, 0.1);
217
+ padding: 2px 8px;
218
+ border-radius: 4px;
219
+ border: 1px solid rgba(255, 255, 255, 0.2);
220
+ }
221
+ </style>
222
+ </head>
223
+ <body>
224
+ <div class="game-wrapper">
225
+ <div class="header">
226
+ <h1>NEON BREAKER</h1>
227
+ <div class="stats">
228
+ <div class="stat">
229
+ <div class="stat-label">Score</div>
230
+ <div class="stat-value" id="score">0</div>
231
+ </div>
232
+ <div class="stat">
233
+ <div class="stat-label">Combo</div>
234
+ <div class="stat-value combo" id="combo">x1</div>
235
+ </div>
236
+ <div class="stat">
237
+ <div class="stat-label">Lives</div>
238
+ <div class="stat-value lives" id="lives">❤️❤️❤️</div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ <canvas id="gameCanvas" width="480" height="600"></canvas>
244
+
245
+ <div class="footer">
246
+ <button class="btn btn-primary" id="startBtn">START</button>
247
+ <button class="btn btn-secondary" id="pauseBtn">PAUSE</button>
248
+ </div>
249
+
250
+ <div class="overlay" id="startScreen">
251
+ <h2>NEON BREAKER</h2>
252
+ <p class="subtitle">Break all the neon blocks!</p>
253
+ <button class="btn btn-primary" id="playBtn">PLAY NOW</button>
254
+ <p class="instructions">
255
+ <kbd>←</kbd> <kbd>→</kbd> or Mouse to move | <kbd>Space</kbd> to launch | <kbd>P</kbd> to pause
256
+ </p>
257
+ </div>
258
+
259
+ <div class="overlay hidden" id="gameOverScreen">
260
+ <h2>GAME OVER</h2>
261
+ <div class="final-score" id="finalScore">0</div>
262
+ <div class="high-score" id="highScoreText">Best: 0</div>
263
+ <button class="btn btn-primary" id="retryBtn">TRY AGAIN</button>
264
+ </div>
265
+
266
+ <div class="overlay hidden" id="winScreen">
267
+ <h2>🎉 YOU WIN! 🎉</h2>
268
+ <div class="final-score" id="winScore">0</div>
269
+ <button class="btn btn-primary" id="nextBtn">PLAY AGAIN</button>
270
+ </div>
271
+
272
+ <div class="overlay hidden" id="pauseScreen">
273
+ <h2>PAUSED</h2>
274
+ <p class="subtitle">Press P or click Resume to continue</p>
275
+ <button class="btn btn-primary" id="resumeBtn">RESUME</button>
276
+ </div>
277
+ </div>
278
+
279
+ <script>
280
+ // Audio Context for sound effects
281
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
282
+ let audioCtx = null;
283
+
284
+ function initAudio() {
285
+ if (!audioCtx) {
286
+ audioCtx = new AudioCtx();
287
+ }
288
+ }
289
+
290
+ function playSound(type) {
291
+ if (!audioCtx) return;
292
+
293
+ const oscillator = audioCtx.createOscillator();
294
+ const gainNode = audioCtx.createGain();
295
+
296
+ oscillator.connect(gainNode);
297
+ gainNode.connect(audioCtx.destination);
298
+
299
+ switch(type) {
300
+ case 'hit':
301
+ oscillator.frequency.setValueAtTime(440, audioCtx.currentTime);
302
+ oscillator.frequency.exponentialRampToValueAtTime(880, audioCtx.currentTime + 0.1);
303
+ gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
304
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
305
+ oscillator.start(audioCtx.currentTime);
306
+ oscillator.stop(audioCtx.currentTime + 0.1);
307
+ break;
308
+ case 'break':
309
+ oscillator.frequency.setValueAtTime(600, audioCtx.currentTime);
310
+ oscillator.frequency.exponentialRampToValueAtTime(1200, audioCtx.currentTime + 0.15);
311
+ gainNode.gain.setValueAtTime(0.4, audioCtx.currentTime);
312
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
313
+ oscillator.start(audioCtx.currentTime);
314
+ oscillator.stop(audioCtx.currentTime + 0.15);
315
+ break;
316
+ case 'powerup':
317
+ oscillator.type = 'sine';
318
+ oscillator.frequency.setValueAtTime(523, audioCtx.currentTime);
319
+ oscillator.frequency.setValueAtTime(659, audioCtx.currentTime + 0.1);
320
+ oscillator.frequency.setValueAtTime(784, audioCtx.currentTime + 0.2);
321
+ gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
322
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
323
+ oscillator.start(audioCtx.currentTime);
324
+ oscillator.stop(audioCtx.currentTime + 0.3);
325
+ break;
326
+ case 'lose':
327
+ oscillator.type = 'sawtooth';
328
+ oscillator.frequency.setValueAtTime(300, audioCtx.currentTime);
329
+ oscillator.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.5);
330
+ gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
331
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.5);
332
+ oscillator.start(audioCtx.currentTime);
333
+ oscillator.stop(audioCtx.currentTime + 0.5);
334
+ break;
335
+ case 'launch':
336
+ oscillator.type = 'sine';
337
+ oscillator.frequency.setValueAtTime(200, audioCtx.currentTime);
338
+ oscillator.frequency.exponentialRampToValueAtTime(600, audioCtx.currentTime + 0.1);
339
+ gainNode.gain.setValueAtTime(0.2, audioCtx.currentTime);
340
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
341
+ oscillator.start(audioCtx.currentTime);
342
+ oscillator.stop(audioCtx.currentTime + 0.1);
343
+ break;
344
+ case 'win':
345
+ const notes = [523, 659, 784, 1047];
346
+ notes.forEach((freq, i) => {
347
+ const osc = audioCtx.createOscillator();
348
+ const gain = audioCtx.createGain();
349
+ osc.connect(gain);
350
+ gain.connect(audioCtx.destination);
351
+ osc.frequency.setValueAtTime(freq, audioCtx.currentTime + i * 0.15);
352
+ gain.gain.setValueAtTime(0.3, audioCtx.currentTime + i * 0.15);
353
+ gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + i * 0.15 + 0.3);
354
+ osc.start(audioCtx.currentTime + i * 0.15);
355
+ osc.stop(audioCtx.currentTime + i * 0.15 + 0.3);
356
+ });
357
+ break;
358
+ }
359
+ }
360
+
361
+ // Game Setup
362
+ const canvas = document.getElementById('gameCanvas');
363
+ const ctx = canvas.getContext('2d');
364
+
365
+ // Game State
366
+ let gameState = 'start'; // start, playing, paused, gameover, win
367
+ let score = 0;
368
+ let highScore = parseInt(localStorage.getItem('neonBreakerHighScore')) || 0;
369
+ let lives = 3;
370
+ let combo = 1;
371
+ let comboTimer = 0;
372
+ let level = 1;
373
+ let shakeTimer = 0;
374
+ let shakeIntensity = 0;
375
+
376
+ // Paddle
377
+ const paddle = {
378
+ width: 100,
379
+ height: 15,
380
+ x: 190,
381
+ y: 560,
382
+ speed: 8,
383
+ dx: 0,
384
+ targetX: 190,
385
+ color: '#00ffff',
386
+ glowColor: 'rgba(0, 255, 255, 0.5)'
387
+ };
388
+
389
+ // Ball
390
+ const ball = {
391
+ radius: 8,
392
+ x: 240,
393
+ y: 540,
394
+ speed: 5,
395
+ dx: 0,
396
+ dy: 0,
397
+ launched: false,
398
+ trail: []
399
+ };
400
+
401
+ // Bricks
402
+ let bricks = [];
403
+ const brickRows = 6;
404
+ const brickCols = 8;
405
+ const brickWidth = 52;
406
+ const brickHeight = 20;
407
+ const brickPadding = 6;
408
+ const brickOffsetTop = 60;
409
+ const brickOffsetLeft = 15;
410
+
411
+ // Power-ups
412
+ let powerups = [];
413
+ const POWERUP_TYPES = {
414
+ WIDE: { color: '#00ff00', symbol: 'W', desc: 'Wide Paddle' },
415
+ MULTI: { color: '#ff00ff', symbol: 'M', desc: 'Multi Ball' },
416
+ FIRE: { color: '#ff6600', symbol: 'F', desc: 'Fire Ball' },
417
+ SLOW: { color: '#00ffff', symbol: 'S', desc: 'Slow Motion' }
418
+ };
419
+ let activePowerups = {};
420
+
421
+ // Particles
422
+ let particles = [];
423
+
424
+ // Colors for brick rows
425
+ const rowColors = [
426
+ { main: '#ff0066', glow: 'rgba(255, 0, 102, 0.5)' },
427
+ { main: '#ff6600', glow: 'rgba(255, 102, 0, 0.5)' },
428
+ { main: '#ffcc00', glow: 'rgba(255, 204, 0, 0.5)' },
429
+ { main: '#00ff66', glow: 'rgba(0, 255, 102, 0.5)' },
430
+ { main: '#00ccff', glow: 'rgba(0, 204, 255, 0.5)' },
431
+ { main: '#cc00ff', glow: 'rgba(204, 0, 255, 0.5)' }
432
+ ];
433
+
434
+ // Input
435
+ let keys = {};
436
+ let mouseX = null;
437
+ let useMouse = false;
438
+
439
+ function createBricks() {
440
+ bricks = [];
441
+ for (let row = 0; row < brickRows; row++) {
442
+ for (let col = 0; col < brickCols; col++) {
443
+ const hits = row < 2 ? 2 : 1; // Top rows need 2 hits
444
+ bricks.push({
445
+ x: col * (brickWidth + brickPadding) + brickOffsetLeft,
446
+ y: row * (brickHeight + brickPadding) + brickOffsetTop,
447
+ width: brickWidth,
448
+ height: brickHeight,
449
+ color: rowColors[row].main,
450
+ glow: rowColors[row].glow,
451
+ hits: hits,
452
+ maxHits: hits,
453
+ visible: true,
454
+ points: (brickRows - row) * 10
455
+ });
456
+ }
457
+ }
458
+ }
459
+
460
+ function resetBall() {
461
+ ball.launched = false;
462
+ ball.x = paddle.x + paddle.width / 2;
463
+ ball.y = paddle.y - ball.radius - 2;
464
+ ball.dx = 0;
465
+ ball.dy = 0;
466
+ ball.trail = [];
467
+ }
468
+
469
+ function launchBall() {
470
+ if (!ball.launched && gameState === 'playing') {
471
+ ball.launched = true;
472
+ const angle = (Math.random() * 60 + 60) * Math.PI / 180; // 60-120 degrees
473
+ ball.dx = ball.speed * Math.cos(angle) * (Math.random() > 0.5 ? 1 : -1);
474
+ ball.dy = -ball.speed * Math.sin(angle);
475
+ playSound('launch');
476
+ }
477
+ }
478
+
479
+ function createParticles(x, y, color, count = 10) {
480
+ for (let i = 0; i < count; i++) {
481
+ particles.push({
482
+ x: x,
483
+ y: y,
484
+ dx: (Math.random() - 0.5) * 8,
485
+ dy: (Math.random() - 0.5) * 8,
486
+ life: 1,
487
+ decay: 0.02 + Math.random() * 0.02,
488
+ size: 2 + Math.random() * 4,
489
+ color: color
490
+ });
491
+ }
492
+ }
493
+
494
+ function spawnPowerup(x, y) {
495
+ if (Math.random() < 0.2) { // 20% chance
496
+ const types = Object.keys(POWERUP_TYPES);
497
+ const type = types[Math.floor(Math.random() * types.length)];
498
+ powerups.push({
499
+ x: x,
500
+ y: y,
501
+ type: type,
502
+ dy: 2,
503
+ ...POWERUP_TYPES[type]
504
+ });
505
+ }
506
+ }
507
+
508
+ function activatePowerup(type) {
509
+ playSound('powerup');
510
+
511
+ // Clear existing powerup of same type
512
+ if (activePowerups[type]) {
513
+ clearTimeout(activePowerups[type]);
514
+ }
515
+
516
+ switch(type) {
517
+ case 'WIDE':
518
+ paddle.width = 150;
519
+ activePowerups[type] = setTimeout(() => { paddle.width = 100; }, 10000);
520
+ break;
521
+ case 'MULTI':
522
+ // Spawn 2 extra balls
523
+ for (let i = 0; i < 2; i++) {
524
+ balls.push({
525
+ x: ball.x,
526
+ y: ball.y,
527
+ dx: ball.dx + (Math.random() - 0.5) * 4,
528
+ dy: ball.dy + (Math.random() - 0.5) * 4,
529
+ radius: ball.radius,
530
+ trail: [],
531
+ isExtra: true
532
+ });
533
+ }
534
+ break;
535
+ case 'FIRE':
536
+ ball.fireball = true;
537
+ activePowerups[type] = setTimeout(() => { ball.fireball = false; }, 8000);
538
+ break;
539
+ case 'SLOW':
540
+ ball.speed = 3;
541
+ activePowerups[type] = setTimeout(() => { ball.speed = 5 + level * 0.5; }, 8000);
542
+ break;
543
+ }
544
+ }
545
+
546
+ // Extra balls for multi-ball powerup
547
+ let balls = [];
548
+
549
+ function update() {
550
+ if (gameState !== 'playing') return;
551
+
552
+ // Update combo timer
553
+ if (comboTimer > 0) {
554
+ comboTimer--;
555
+ if (comboTimer === 0) {
556
+ combo = 1;
557
+ updateUI();
558
+ }
559
+ }
560
+
561
+ // Update shake
562
+ if (shakeTimer > 0) {
563
+ shakeTimer--;
564
+ }
565
+
566
+ // Paddle movement
567
+ if (useMouse && mouseX !== null) {
568
+ paddle.targetX = mouseX - paddle.width / 2;
569
+ paddle.x += (paddle.targetX - paddle.x) * 0.2;
570
+ } else {
571
+ if (keys['ArrowLeft'] || keys['a']) {
572
+ paddle.x -= paddle.speed;
573
+ }
574
+ if (keys['ArrowRight'] || keys['d']) {
575
+ paddle.x += paddle.speed;
576
+ }
577
+ }
578
+
579
+ // Clamp paddle
580
+ paddle.x = Math.max(0, Math.min(canvas.width - paddle.width, paddle.x));
581
+
582
+ // Ball follows paddle if not launched
583
+ if (!ball.launched) {
584
+ ball.x = paddle.x + paddle.width / 2;
585
+ ball.y = paddle.y - ball.radius - 2;
586
+ return;
587
+ }
588
+
589
+ // Update all balls
590
+ const allBalls = [ball, ...balls];
591
+
592
+ for (let i = allBalls.length - 1; i >= 0; i--) {
593
+ const b = allBalls[i];
594
+
595
+ // Add to trail
596
+ b.trail.push({ x: b.x, y: b.y });
597
+ if (b.trail.length > 10) b.trail.shift();
598
+
599
+ // Move ball
600
+ b.x += b.dx;
601
+ b.y += b.dy;
602
+
603
+ // Wall collisions
604
+ if (b.x - b.radius < 0 || b.x + b.radius > canvas.width) {
605
+ b.dx *= -1;
606
+ b.x = Math.max(b.radius, Math.min(canvas.width - b.radius, b.x));
607
+ playSound('hit');
608
+ }
609
+ if (b.y - b.radius < 0) {
610
+ b.dy *= -1;
611
+ b.y = b.radius;
612
+ playSound('hit');
613
+ }
614
+
615
+ // Bottom - lose ball
616
+ if (b.y + b.radius > canvas.height) {
617
+ if (b.isExtra) {
618
+ balls.splice(balls.indexOf(b), 1);
619
+ } else {
620
+ lives--;
621
+ updateUI();
622
+ playSound('lose');
623
+ shakeTimer = 15;
624
+ shakeIntensity = 10;
625
+
626
+ if (lives <= 0) {
627
+ gameOver();
628
+ } else {
629
+ resetBall();
630
+ }
631
+ }
632
+ continue;
633
+ }
634
+
635
+ // Paddle collision
636
+ if (b.y + b.radius > paddle.y &&
637
+ b.y - b.radius < paddle.y + paddle.height &&
638
+ b.x > paddle.x &&
639
+ b.x < paddle.x + paddle.width &&
640
+ b.dy > 0) {
641
+
642
+ // Calculate bounce angle based on where ball hits paddle
643
+ const hitPos = (b.x - paddle.x) / paddle.width;
644
+ const angle = (hitPos - 0.5) * Math.PI * 0.7; // -63 to +63 degrees
645
+ const speed = Math.sqrt(b.dx * b.dx + b.dy * b.dy);
646
+
647
+ b.dx = speed * Math.sin(angle);
648
+ b.dy = -Math.abs(speed * Math.cos(angle));
649
+
650
+ playSound('hit');
651
+ createParticles(b.x, paddle.y, paddle.color, 5);
652
+ }
653
+
654
+ // Brick collisions
655
+ for (let brick of bricks) {
656
+ if (!brick.visible) continue;
657
+
658
+ if (b.x + b.radius > brick.x &&
659
+ b.x - b.radius < brick.x + brick.width &&
660
+ b.y + b.radius > brick.y &&
661
+ b.y - b.radius < brick.y + brick.height) {
662
+
663
+ // Determine collision side
664
+ const overlapLeft = b.x + b.radius - brick.x;
665
+ const overlapRight = brick.x + brick.width - (b.x - b.radius);
666
+ const overlapTop = b.y + b.radius - brick.y;
667
+ const overlapBottom = brick.y + brick.height - (b.y - b.radius);
668
+
669
+ const minOverlapX = Math.min(overlapLeft, overlapRight);
670
+ const minOverlapY = Math.min(overlapTop, overlapBottom);
671
+
672
+ if (!b.fireball) {
673
+ if (minOverlapX < minOverlapY) {
674
+ b.dx *= -1;
675
+ } else {
676
+ b.dy *= -1;
677
+ }
678
+ }
679
+
680
+ brick.hits--;
681
+
682
+ if (brick.hits <= 0) {
683
+ brick.visible = false;
684
+ score += brick.points * combo;
685
+ combo++;
686
+ comboTimer = 120; // 2 seconds at 60fps
687
+ updateUI();
688
+
689
+ playSound('break');
690
+ createParticles(
691
+ brick.x + brick.width / 2,
692
+ brick.y + brick.height / 2,
693
+ brick.color,
694
+ 15
695
+ );
696
+ spawnPowerup(brick.x + brick.width / 2, brick.y + brick.height / 2);
697
+
698
+ shakeTimer = 5;
699
+ shakeIntensity = 3;
700
+ } else {
701
+ playSound('hit');
702
+ createParticles(b.x, b.y, brick.color, 5);
703
+ }
704
+
705
+ if (!b.fireball) break;
706
+ }
707
+ }
708
+ }
709
+
710
+ // Update powerups
711
+ for (let i = powerups.length - 1; i >= 0; i--) {
712
+ const p = powerups[i];
713
+ p.y += p.dy;
714
+
715
+ // Check paddle collision
716
+ if (p.y + 10 > paddle.y &&
717
+ p.y - 10 < paddle.y + paddle.height &&
718
+ p.x > paddle.x &&
719
+ p.x < paddle.x + paddle.width) {
720
+
721
+ activatePowerup(p.type);
722
+ powerups.splice(i, 1);
723
+ continue;
724
+ }
725
+
726
+ // Remove if off screen
727
+ if (p.y > canvas.height) {
728
+ powerups.splice(i, 1);
729
+ }
730
+ }
731
+
732
+ // Update particles
733
+ for (let i = particles.length - 1; i >= 0; i--) {
734
+ const p = particles[i];
735
+ p.x += p.dx;
736
+ p.y += p.dy;
737
+ p.dy += 0.1; // gravity
738
+ p.life -= p.decay;
739
+
740
+ if (p.life <= 0) {
741
+ particles.splice(i, 1);
742
+ }
743
+ }
744
+
745
+ // Check win condition
746
+ if (bricks.every(b => !b.visible)) {
747
+ levelWin();
748
+ }
749
+ }
750
+
751
+ function draw() {
752
+ // Apply screen shake
753
+ ctx.save();
754
+ if (shakeTimer > 0) {
755
+ const dx = (Math.random() - 0.5) * shakeIntensity;
756
+ const dy = (Math.random() - 0.5) * shakeIntensity;
757
+ ctx.translate(dx, dy);
758
+ }
759
+
760
+ // Clear canvas
761
+ ctx.fillStyle = '#050510';
762
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
763
+
764
+ // Draw grid background
765
+ ctx.strokeStyle = 'rgba(0, 255, 255, 0.03)';
766
+ ctx.lineWidth = 1;
767
+ for (let x = 0; x < canvas.width; x += 30) {
768
+ ctx.beginPath();
769
+ ctx.moveTo(x, 0);
770
+ ctx.lineTo(x, canvas.height);
771
+ ctx.stroke();
772
+ }
773
+ for (let y = 0; y < canvas.height; y += 30) {
774
+ ctx.beginPath();
775
+ ctx.moveTo(0, y);
776
+ ctx.lineTo(canvas.width, y);
777
+ ctx.stroke();
778
+ }
779
+
780
+ // Draw bricks
781
+ for (let brick of bricks) {
782
+ if (!brick.visible) continue;
783
+
784
+ // Glow effect
785
+ ctx.shadowBlur = 15;
786
+ ctx.shadowColor = brick.glow;
787
+
788
+ // Brick body
789
+ const gradient = ctx.createLinearGradient(brick.x, brick.y, brick.x, brick.y + brick.height);
790
+ gradient.addColorStop(0, brick.color);
791
+ gradient.addColorStop(1, adjustColor(brick.color, -30));
792
+ ctx.fillStyle = gradient;
793
+
794
+ ctx.beginPath();
795
+ ctx.roundRect(brick.x, brick.y, brick.width, brick.height, 3);
796
+ ctx.fill();
797
+
798
+ // Hit indicator for multi-hit bricks
799
+ if (brick.maxHits > 1 && brick.hits > 1) {
800
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
801
+ ctx.font = 'bold 12px Arial';
802
+ ctx.textAlign = 'center';
803
+ ctx.fillText(brick.hits, brick.x + brick.width / 2, brick.y + brick.height / 2 + 4);
804
+ }
805
+
806
+ ctx.shadowBlur = 0;
807
+ }
808
+
809
+ // Draw powerups
810
+ for (let p of powerups) {
811
+ ctx.shadowBlur = 15;
812
+ ctx.shadowColor = p.color;
813
+ ctx.fillStyle = p.color;
814
+
815
+ ctx.beginPath();
816
+ ctx.arc(p.x, p.y, 12, 0, Math.PI * 2);
817
+ ctx.fill();
818
+
819
+ ctx.fillStyle = '#000';
820
+ ctx.font = 'bold 12px Arial';
821
+ ctx.textAlign = 'center';
822
+ ctx.textBaseline = 'middle';
823
+ ctx.fillText(p.symbol, p.x, p.y);
824
+
825
+ ctx.shadowBlur = 0;
826
+ }
827
+
828
+ // Draw particles
829
+ for (let p of particles) {
830
+ ctx.globalAlpha = p.life;
831
+ ctx.fillStyle = p.color;
832
+ ctx.beginPath();
833
+ ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
834
+ ctx.fill();
835
+ }
836
+ ctx.globalAlpha = 1;
837
+
838
+ // Draw ball trail
839
+ const allBalls = [ball, ...balls];
840
+ for (let b of allBalls) {
841
+ for (let i = 0; i < b.trail.length; i++) {
842
+ const t = b.trail[i];
843
+ const alpha = i / b.trail.length * 0.5;
844
+ const size = b.radius * (i / b.trail.length);
845
+
846
+ ctx.globalAlpha = alpha;
847
+ ctx.fillStyle = b.fireball ? '#ff6600' : '#ffffff';
848
+ ctx.beginPath();
849
+ ctx.arc(t.x, t.y, size, 0, Math.PI * 2);
850
+ ctx.fill();
851
+ }
852
+ ctx.globalAlpha = 1;
853
+
854
+ // Draw ball
855
+ ctx.shadowBlur = 20;
856
+ ctx.shadowColor = b.fireball ? '#ff6600' : '#ffffff';
857
+ ctx.fillStyle = b.fireball ? '#ff6600' : '#ffffff';
858
+ ctx.beginPath();
859
+ ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
860
+ ctx.fill();
861
+
862
+ // Inner glow
863
+ const ballGradient = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.radius);
864
+ ballGradient.addColorStop(0, 'rgba(255, 255, 255, 0.8)');
865
+ ballGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
866
+ ctx.fillStyle = ballGradient;
867
+ ctx.beginPath();
868
+ ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
869
+ ctx.fill();
870
+
871
+ ctx.shadowBlur = 0;
872
+ }
873
+
874
+ // Draw paddle
875
+ ctx.shadowBlur = 20;
876
+ ctx.shadowColor = paddle.glowColor;
877
+
878
+ const paddleGradient = ctx.createLinearGradient(paddle.x, paddle.y, paddle.x, paddle.y + paddle.height);
879
+ paddleGradient.addColorStop(0, '#ffffff');
880
+ paddleGradient.addColorStop(0.3, paddle.color);
881
+ paddleGradient.addColorStop(1, adjustColor(paddle.color, -50));
882
+ ctx.fillStyle = paddleGradient;
883
+
884
+ ctx.beginPath();
885
+ ctx.roundRect(paddle.x, paddle.y, paddle.width, paddle.height, 5);
886
+ ctx.fill();
887
+
888
+ // Paddle highlight
889
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
890
+ ctx.beginPath();
891
+ ctx.roundRect(paddle.x + 5, paddle.y + 2, paddle.width - 10, 4, 2);
892
+ ctx.fill();
893
+
894
+ ctx.shadowBlur = 0;
895
+
896
+ // Draw active powerup indicators
897
+ let indicatorY = 30;
898
+ ctx.font = '12px Arial';
899
+ ctx.textAlign = 'left';
900
+ for (let type in activePowerups) {
901
+ const p = POWERUP_TYPES[type];
902
+ ctx.fillStyle = p.color;
903
+ ctx.fillText(`${p.symbol}: ${p.desc}`, 10, indicatorY);
904
+ indicatorY += 18;
905
+ }
906
+
907
+ ctx.restore();
908
+ }
909
+
910
+ function adjustColor(color, amount) {
911
+ const hex = color.replace('#', '');
912
+ const r = Math.max(0, Math.min(255, parseInt(hex.substr(0, 2), 16) + amount));
913
+ const g = Math.max(0, Math.min(255, parseInt(hex.substr(2, 2), 16) + amount));
914
+ const b = Math.max(0, Math.min(255, parseInt(hex.substr(4, 2), 16) + amount));
915
+ return `rgb(${r}, ${g}, ${b})`;
916
+ }
917
+
918
+ function updateUI() {
919
+ document.getElementById('score').textContent = score;
920
+ document.getElementById('combo').textContent = `x${combo}`;
921
+ document.getElementById('lives').textContent = '❤️'.repeat(Math.max(0, lives));
922
+ }
923
+
924
+ function gameOver() {
925
+ gameState = 'gameover';
926
+ playSound('lose');
927
+
928
+ if (score > highScore) {
929
+ highScore = score;
930
+ localStorage.setItem('neonBreakerHighScore', highScore);
931
+ }
932
+
933
+ document.getElementById('finalScore').textContent = `Score: ${score}`;
934
+ document.getElementById('highScoreText').textContent = `Best: ${highScore}`;
935
+ document.getElementById('gameOverScreen').classList.remove('hidden');
936
+ }
937
+
938
+ function levelWin() {
939
+ gameState = 'win';
940
+ playSound('win');
941
+ document.getElementById('winScore').textContent = `Score: ${score}`;
942
+ document.getElementById('winScreen').classList.remove('hidden');
943
+ }
944
+
945
+ function startGame() {
946
+ initAudio();
947
+ gameState = 'playing';
948
+ score = 0;
949
+ lives = 3;
950
+ combo = 1;
951
+ comboTimer = 0;
952
+ level = 1;
953
+ balls = [];
954
+ powerups = [];
955
+ particles = [];
956
+ activePowerups = {};
957
+ paddle.width = 100;
958
+ paddle.x = canvas.width / 2 - paddle.width / 2;
959
+ ball.speed = 5;
960
+ ball.fireball = false;
961
+
962
+ createBricks();
963
+ resetBall();
964
+ updateUI();
965
+
966
+ document.getElementById('startScreen').classList.add('hidden');
967
+ document.getElementById('gameOverScreen').classList.add('hidden');
968
+ document.getElementById('winScreen').classList.add('hidden');
969
+ document.getElementById('pauseScreen').classList.add('hidden');
970
+ }
971
+
972
+ function togglePause() {
973
+ if (gameState === 'playing') {
974
+ gameState = 'paused';
975
+ document.getElementById('pauseScreen').classList.remove('hidden');
976
+ } else if (gameState === 'paused') {
977
+ gameState = 'playing';
978
+ document.getElementById('pauseScreen').classList.add('hidden');
979
+ }
980
+ }
981
+
982
+ // Game Loop
983
+ function gameLoop() {
984
+ update();
985
+ draw();
986
+ requestAnimationFrame(gameLoop);
987
+ }
988
+
989
+ // Event Listeners
990
+ document.addEventListener('keydown', (e) => {
991
+ keys[e.key] = true;
992
+ useMouse = false;
993
+
994
+ if (e.key === ' ' || e.key === 'Space') {
995
+ e.preventDefault();
996
+ if (gameState === 'playing') {
997
+ launchBall();
998
+ }
999
+ }
1000
+
1001
+ if (e.key === 'p' || e.key === 'P') {
1002
+ togglePause();
1003
+ }
1004
+ });
1005
+
1006
+ document.addEventListener('keyup', (e) => {
1007
+ keys[e.key] = false;
1008
+ });
1009
+
1010
+ canvas.addEventListener('mousemove', (e) => {
1011
+ const rect = canvas.getBoundingClientRect();
1012
+ mouseX = e.clientX - rect.left;
1013
+ useMouse = true;
1014
+ });
1015
+
1016
+ canvas.addEventListener('click', () => {
1017
+ if (gameState === 'playing') {
1018
+ launchBall();
1019
+ }
1020
+ });
1021
+
1022
+ // Button listeners
1023
+ document.getElementById('playBtn').addEventListener('click', startGame);
1024
+ document.getElementById('startBtn').addEventListener('click', startGame);
1025
+ document.getElementById('pauseBtn').addEventListener('click', togglePause);
1026
+ document.getElementById('resumeBtn').addEventListener('click', togglePause);
1027
+ document.getElementById('retryBtn').addEventListener('click', startGame);
1028
+ document.getElementById('nextBtn').addEventListener('click', startGame);
1029
+
1030
+ // Initialize
1031
+ createBricks();
1032
+ resetBall();
1033
+ updateUI();
1034
+ gameLoop();
1035
+ </script>
1036
+ </body>
1037
+ </html>