offerpk3 commited on
Commit
ccb585f
·
verified ·
1 Parent(s): aa9da8a

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +357 -266
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
- <title>Advanced Bubble Shooter</title>
7
  <style>
8
  body {
9
  margin: 0;
@@ -14,28 +14,28 @@
14
  align-items: center;
15
  min-height: 100vh;
16
  font-family: 'Arial', sans-serif;
17
- overflow: hidden; /* Prevent scrollbars from touch */
18
- touch-action: none; /* Disable default touch actions like pinch-zoom */
19
  }
20
 
21
  .game-wrapper {
22
- position: relative; /* For absolute positioning of UI elements over canvas */
23
  }
24
 
25
  .game-container {
26
  background: rgba(255, 255, 255, 0.1);
27
  border-radius: 20px;
28
- padding: 15px; /* Reduced padding */
29
  backdrop-filter: blur(10px);
30
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
31
  text-align: center;
32
- width: fit-content; /* Adjust to canvas */
33
  }
34
 
35
  canvas {
36
  border: 2px solid #fff;
37
  border-radius: 10px;
38
- background: #000022; /* Even darker blue */
39
  display: block;
40
  margin: 0 auto;
41
  }
@@ -46,7 +46,7 @@
46
  display: flex;
47
  justify-content: space-around;
48
  align-items: center;
49
- font-size: 16px; /* Slightly smaller UI text */
50
  }
51
 
52
  .score, .shots-info {
@@ -70,17 +70,17 @@
70
  font-weight: bold;
71
  text-align: center;
72
  z-index: 100;
73
- border-radius: 10px; /* Match canvas */
74
  }
75
  .overlay-message h2 {
76
  margin-bottom: 20px;
77
  font-size: 1.5em;
78
  }
79
 
80
- .controls-info { /* Renamed from .controls */
81
  margin-top: 10px;
82
  color: rgba(255, 255, 255, 0.85);
83
- font-size: 12px; /* Smaller instructions */
84
  }
85
 
86
  button.game-button {
@@ -102,8 +102,6 @@
102
  button.game-button:active {
103
  transform: scale(0.95);
104
  }
105
-
106
- /* Specific styling for Next Bubble area to indicate tappability */
107
  .next-bubble-display {
108
  cursor: pointer;
109
  padding: 5px;
@@ -123,7 +121,7 @@
123
  <div class="score">Score: <span id="score">0</span></div>
124
  <div class="shots-info">Next Row: <span id="shotsUntilNextRow">0</span></div>
125
  </div>
126
- <canvas id="gameCanvas"></canvas> <!-- Width/Height set by JS -->
127
  <div class="controls-info">
128
  Aim & Shoot: Drag from shooter base / Click<br>
129
  Connect 3+ same colors. Tap "Next" to swap.
@@ -137,7 +135,7 @@
137
  <button id="menuButton" class="game-button">Main Menu</button>
138
  </div>
139
  <div id="startScreen" class="overlay-message">
140
- <h2>Bubble Shooter</h2>
141
  <p>Ready to pop some bubbles?</p>
142
  <button id="startButton" class="game-button">Start Game</button>
143
  </div>
@@ -158,7 +156,6 @@
158
  const menuButton = document.getElementById('menuButton');
159
  const startButton = document.getElementById('startButton');
160
 
161
- // Game States
162
  const GAME_STATE = {
163
  MENU: 'MENU',
164
  PLAYING: 'PLAYING',
@@ -167,27 +164,28 @@
167
  };
168
  let currentGameState = GAME_STATE.MENU;
169
 
170
- // Game Constants - Declared here, values set in setupCanvas
171
  let BUBBLE_RADIUS = 0;
172
  let BUBBLE_DIAMETER = 0;
173
  let ROWS = 0;
174
- let COLS = 12; // Fixed columns, radius will adjust
175
  let GAME_OVER_ROW_INDEX = 0;
176
 
177
  const INIT_ROWS_COUNT_TARGET = 5;
178
  const SHOTS_UNTIL_NEW_ROW_THRESHOLD = 6;
 
179
 
180
  const BUBBLE_COLORS = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3', '#54a0ff'];
181
 
182
  const POWERUP_TYPE = {
183
  NONE: 'NONE',
184
  BOMB: 'BOMB',
185
- RAINBOW: 'RAINBOW'
 
186
  };
187
- const BOMB_RADIUS_MULTIPLIER = 2.5;
188
- const POWERUP_CHANCE = 0.15;
 
189
 
190
- // Game Variables
191
  let score = 0;
192
  let shotsFiredSinceLastRow = 0;
193
  const bubbleGrid = [];
@@ -197,38 +195,29 @@
197
  let particles = [];
198
 
199
  let isAiming = false;
200
- let aimStartX = 0;
201
- let aimStartY = 0;
202
 
203
  function setupCanvas() {
204
- console.log("--- Running setupCanvas ---");
205
  const screenWidth = window.innerWidth;
206
  const screenHeight = window.innerHeight;
207
-
208
  const maxCanvasWidth = Math.min(screenWidth * 0.95, 500);
209
 
210
- // COLS is fixed
211
  BUBBLE_RADIUS = Math.floor(maxCanvasWidth / ( (COLS + 0.5) * 2));
212
  BUBBLE_DIAMETER = BUBBLE_RADIUS * 2;
213
- console.log(`Calculated BUBBLE_RADIUS: ${BUBBLE_RADIUS}, BUBBLE_DIAMETER: ${BUBBLE_DIAMETER}`);
214
 
215
  canvas.width = Math.floor((COLS + 0.5) * BUBBLE_DIAMETER);
216
 
217
  const bubbleRowHeight = BUBBLE_RADIUS * 2 * 0.866;
218
- // Calculate ROWS based on available height
219
- const availableGridHeight = Math.min(screenHeight * 0.70, canvas.width * 1.3); // Space for grid
220
  ROWS = Math.floor(availableGridHeight / bubbleRowHeight);
221
- ROWS = Math.max(ROWS, 10); // Ensure minimum number of ROWS (e.g., 10) for reasonable play
222
 
223
- // Canvas height includes grid + shooter space + small bottom margin
224
  canvas.height = Math.floor(ROWS * bubbleRowHeight + BUBBLE_DIAMETER * 3.5);
225
-
226
- // CRITICAL: Set GAME_OVER_ROW_INDEX based on the calculated ROWS
227
  GAME_OVER_ROW_INDEX = ROWS - 1;
228
- console.log(`Canvas W: ${canvas.width}, H: ${canvas.height}. COLS: ${COLS}, ROWS: ${ROWS}. GAME_OVER_ROW_INDEX: ${GAME_OVER_ROW_INDEX}`);
229
 
230
  shooter.x = canvas.width / 2;
231
- shooter.y = canvas.height - BUBBLE_DIAMETER * 1.8; // Position shooter near bottom
232
 
233
  shooter.nextBubbleArea = {
234
  x: shooter.x + BUBBLE_DIAMETER * 1.5,
@@ -236,13 +225,11 @@
236
  width: BUBBLE_DIAMETER * 1.2,
237
  height: BUBBLE_DIAMETER * 1.2
238
  };
239
- console.log("--- setupCanvas Finished ---");
240
  }
241
 
242
  function initGame() {
243
- console.log("--- Running initGame ---");
244
- // At this point, ROWS and GAME_OVER_ROW_INDEX MUST be correctly set by setupCanvas
245
-
246
  currentGameState = GAME_STATE.PLAYING;
247
  overlayScreen.style.display = 'none';
248
  startScreen.style.display = 'none';
@@ -253,34 +240,27 @@
253
  fallingBubbles = [];
254
  particles = [];
255
 
256
- bubbleGrid.length = 0; // Clear previous grid
257
  for (let r = 0; r < ROWS; r++) {
258
  bubbleGrid[r] = new Array(getColsInRow(r)).fill(null);
259
  }
260
 
261
- // Determine initial rows to fill, ensuring ample empty space at the bottom
262
- // Leave at least 4-5 empty rows below the initial bubbles.
263
  let initRowsToFill = Math.min(INIT_ROWS_COUNT_TARGET, ROWS - 5);
264
- initRowsToFill = Math.max(1, initRowsToFill); // Ensure at least 1 row if ROWS is very small
265
 
266
- console.log(`Initializing game grid: Total ROWS=${ROWS}, initRowsToFill=${initRowsToFill}, GAME_OVER_ROW_INDEX=${GAME_OVER_ROW_INDEX}`);
267
 
268
- if (initRowsToFill >= GAME_OVER_ROW_INDEX ) {
269
- console.error(`CRITICAL: initRowsToFill (${initRowsToFill}) is too close to or exceeds GAME_OVER_ROW_INDEX (${GAME_OVER_ROW_INDEX}). Reducing initRowsToFill.`);
270
- initRowsToFill = Math.max(1, GAME_OVER_ROW_INDEX - 2); // Drastic reduction if too close
271
  }
272
-
273
-
274
- if (initRowsToFill < 1 && ROWS > 0) { // If logic above results in 0 rows to fill but ROWS is positive.
275
- console.warn("initRowsToFill calculated as < 1, but ROWS > 0. Setting to 1 row.");
276
- initRowsToFill = 1;
277
- } else if (ROWS === 0) {
278
- console.error("CRITICAL: ROWS is 0. Cannot initialize game grid.");
279
- triggerGameOver("Error: Game grid could not be initialized."); // Or a more specific error state
280
  return;
281
  }
282
 
283
-
284
  for (let r = 0; r < initRowsToFill; r++) {
285
  for (let c = 0; c < getColsInRow(r); c++) {
286
  if (Math.random() < 0.7) {
@@ -293,7 +273,7 @@
293
  shooter.nextBubble = createShooterBubble();
294
  updateScoreDisplay();
295
  updateShotsDisplay();
296
- console.log("--- initGame Finished ---");
297
  }
298
 
299
  function getColsInRow(row) {
@@ -306,7 +286,7 @@
306
  }
307
 
308
  function getBubbleY(row) {
309
- const gridTopMargin = BUBBLE_RADIUS * 0.5; // Small margin at the top of the grid
310
  return row * (BUBBLE_RADIUS * 2 * 0.866) + BUBBLE_RADIUS + gridTopMargin;
311
  }
312
 
@@ -323,27 +303,35 @@
323
 
324
  function createShooterBubble() {
325
  const existingColors = new Set();
 
326
  for (let r = 0; r < ROWS; r++) {
327
  if (!bubbleGrid[r]) continue;
328
  for (let c = 0; c < getColsInRow(r); c++) {
329
  if (bubbleGrid[r][c] && bubbleGrid[r][c].powerUpType === POWERUP_TYPE.NONE) {
330
  existingColors.add(bubbleGrid[r][c].color);
 
331
  }
332
  }
333
  }
334
  let availableColors = Array.from(existingColors);
335
- if (availableColors.length === 0) availableColors = [...BUBBLE_COLORS];
 
 
336
 
337
  let powerUp = POWERUP_TYPE.NONE;
338
  let color = availableColors[Math.floor(Math.random() * availableColors.length)];
339
 
340
  if (Math.random() < POWERUP_CHANCE) {
341
- const randPowerUp = Math.random();
342
- if (randPowerUp < 0.5) {
 
 
 
343
  powerUp = POWERUP_TYPE.BOMB;
344
  color = '#333333';
345
- } else {
346
  powerUp = POWERUP_TYPE.RAINBOW;
 
347
  }
348
  }
349
 
@@ -356,34 +344,46 @@
356
 
357
  function drawBubble(bubble) {
358
  const { x, y, color, radius, powerUpType } = bubble;
359
- if (!radius || radius <= 0) {
360
- // console.warn("Attempting to draw bubble with invalid radius:", bubble); // Can be noisy
361
- return;
362
- }
363
  ctx.beginPath();
364
  ctx.arc(x, y, radius, 0, Math.PI * 2);
365
 
366
  if (powerUpType === POWERUP_TYPE.RAINBOW) {
367
- const rainbowGradient = ctx.createLinearGradient(x - radius, y, x + radius, y);
368
- BUBBLE_COLORS.forEach((c, i) => rainbowGradient.addColorStop(i / BUBBLE_COLORS.length, c));
 
369
  ctx.fillStyle = rainbowGradient;
370
- } else {
 
 
 
 
 
 
 
 
371
  ctx.fillStyle = color;
372
  }
373
  ctx.fill();
374
 
375
- const gradient = ctx.createRadialGradient(x - radius*0.3, y - radius*0.3, 0, x, y, radius);
376
- gradient.addColorStop(0, 'rgba(255, 255, 255, 0.4)');
377
- gradient.addColorStop(0.8, 'rgba(255, 255, 255, 0)');
378
- ctx.fillStyle = gradient;
 
379
  ctx.fill();
380
 
 
 
 
 
381
  if (powerUpType === POWERUP_TYPE.BOMB) {
382
- ctx.fillStyle = 'white';
383
- ctx.font = `${radius * 0.8}px Arial`;
384
- ctx.textAlign = 'center';
385
- ctx.textBaseline = 'middle';
386
- ctx.fillText('B', x, y + radius * 0.1);
387
  }
388
 
389
  ctx.strokeStyle = 'rgba(0,0,0,0.15)';
@@ -391,6 +391,92 @@
391
  ctx.stroke();
392
  }
393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  function drawGrid() {
395
  for (let r = 0; r < ROWS; r++) {
396
  if (!bubbleGrid[r]) continue;
@@ -403,30 +489,18 @@
403
  }
404
 
405
  function drawShooter() {
 
406
  ctx.fillStyle = '#444';
407
  ctx.beginPath();
408
  ctx.arc(shooter.x, shooter.y, BUBBLE_RADIUS * 1.2, Math.PI, 0);
409
  ctx.closePath();
410
  ctx.fill();
411
-
412
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
413
  ctx.lineWidth = 2;
414
- ctx.setLineDash([3, 3]);
415
-
416
- let lineX = shooter.x + Math.cos(shooter.angle) * canvas.height;
417
- let lineY = shooter.y + Math.sin(shooter.angle) * canvas.height;
418
-
419
- if (lineX < BUBBLE_RADIUS) {
420
- lineX = BUBBLE_RADIUS + (BUBBLE_RADIUS - lineX);
421
- } else if (lineX > canvas.width - BUBBLE_RADIUS) {
422
- lineX = (canvas.width - BUBBLE_RADIUS) - (lineX - (canvas.width - BUBBLE_RADIUS));
423
- }
424
-
425
- ctx.beginPath();
426
- ctx.moveTo(shooter.x, shooter.y);
427
- ctx.lineTo(shooter.x + Math.cos(shooter.angle) * 70, shooter.y + Math.sin(shooter.angle) * 70);
428
  ctx.stroke();
429
- ctx.setLineDash([]);
 
 
430
 
431
  if (shooter.currentBubble) {
432
  drawBubble({ ...shooter.currentBubble, x: shooter.x, y: shooter.y });
@@ -488,23 +562,23 @@
488
  }
489
  }
490
 
491
- function createParticles(x, y, color, count = 10) {
492
  for (let i = 0; i < count; i++) {
493
  particles.push({
494
  x, y, color,
495
- vx: (Math.random() - 0.5) * 4,
496
- vy: (Math.random() - 0.5) * 4,
497
- size: Math.random() * 3 + 2,
498
- life: Math.random() * 30 + 30,
499
  alpha: 1
500
  });
501
  }
502
  }
503
 
504
  function shoot() {
505
- console.log("--- shoot() called ---");
506
  if (currentGameState !== GAME_STATE.PLAYING || !shooter.currentBubble || movingBubble) {
507
- console.log(`shoot() aborted: gameState=${currentGameState}, currentBubble=${!!shooter.currentBubble}, movingBubble=${!!movingBubble}`);
508
  return;
509
  }
510
 
@@ -512,10 +586,10 @@
512
  ...shooter.currentBubble,
513
  x: shooter.x,
514
  y: shooter.y,
515
- vx: Math.cos(shooter.angle) * (BUBBLE_DIAMETER * 0.6),
516
- vy: Math.sin(shooter.angle) * (BUBBLE_DIAMETER * 0.6)
517
  };
518
- console.log("Moving bubble created:", movingBubble);
519
 
520
  shooter.currentBubble = shooter.nextBubble;
521
  shooter.nextBubble = createShooterBubble();
@@ -537,8 +611,8 @@
537
  }
538
 
539
  if (movingBubble.y <= BUBBLE_RADIUS) {
540
- console.log("Moving bubble hit top of screen.");
541
- movingBubble.y = BUBBLE_RADIUS; // Snap to top
542
  handleBubbleLanded();
543
  return;
544
  }
@@ -547,10 +621,10 @@
547
  if (!bubbleGrid[r]) continue;
548
  for (let c = 0; c < getColsInRow(r); c++) {
549
  if (bubbleGrid[r][c]) {
550
- const bubble = bubbleGrid[r][c]; // This is the stationary grid bubble
551
  const dist = Math.hypot(movingBubble.x - bubble.x, movingBubble.y - bubble.y);
552
- if (dist < BUBBLE_DIAMETER * 0.95) { // Collision threshold
553
- console.log(`Moving bubble collided with grid bubble at [${r}][${c}]`);
554
  handleBubbleLanded();
555
  return;
556
  }
@@ -560,55 +634,47 @@
560
  }
561
 
562
  function handleBubbleLanded() {
563
- console.log("--- handleBubbleLanded() ---");
564
  if (currentGameState !== GAME_STATE.PLAYING) {
565
- console.log("handleBubbleLanded: Game not playing, aborting.");
566
  if (movingBubble) movingBubble = null;
567
  return;
568
  }
569
- if(!movingBubble) {
570
- console.warn("handleBubbleLanded called, but movingBubble is already null!");
571
- return;
572
- }
573
 
574
  const landedBubbleData = { ...movingBubble };
575
  movingBubble = null;
576
 
577
- console.log(`Landed bubble data: x=${landedBubbleData.x.toFixed(1)}, y=${landedBubbleData.y.toFixed(1)}, type=${landedBubbleData.powerUpType}, color=${landedBubbleData.color}`);
578
- console.log(`Current GAME_OVER_ROW_INDEX for checks: ${GAME_OVER_ROW_INDEX}`);
579
-
580
 
581
  if (landedBubbleData.powerUpType === POWERUP_TYPE.BOMB) {
582
- console.log("Landed bubble is BOMB.");
583
  explodeBomb(landedBubbleData.x, landedBubbleData.y);
584
- } else {
585
- console.log("Landed bubble is NORMAL or RAINBOW. Finding best grid slot...");
 
 
586
  const { row, col } = findBestGridSlot(landedBubbleData.x, landedBubbleData.y);
587
- console.log(`findBestGridSlot returned: target row=${row}, col=${col}.`);
588
 
589
  if (row !== -1 && col !== -1 && row < ROWS && col < getColsInRow(row) && bubbleGrid[row] && (bubbleGrid[row][col] === null || bubbleGrid[row][col] === undefined) ) {
590
  bubbleGrid[row][col] = createGridBubble(col, row, landedBubbleData.color, landedBubbleData.powerUpType);
591
- console.log(`Placed bubble at [${row}][${col}]. Color: ${bubbleGrid[row][col].color}, Type: ${bubbleGrid[row][col].powerUpType}`);
592
 
593
  if (landedBubbleData.powerUpType === POWERUP_TYPE.RAINBOW) {
594
- console.log("Activating RAINBOW effect.");
595
  activateRainbow(row, col);
596
  } else {
597
- console.log("Checking matches for normal bubble.");
598
  checkMatches(row, col, landedBubbleData.color);
599
  }
600
 
601
  if (currentGameState === GAME_STATE.PLAYING && bubbleGrid[row] && bubbleGrid[row][col]) {
602
- console.log(`Bubble at [${row}][${col}] still exists. Game Over Check: landed row (${row}) >= GAME_OVER_ROW_INDEX (${GAME_OVER_ROW_INDEX})`);
603
  if (row >= GAME_OVER_ROW_INDEX) {
604
  triggerGameOver(`Game Over: Bubble landed at row ${row}, which is >= game over line ${GAME_OVER_ROW_INDEX}.`);
605
  return;
606
  }
607
- } else {
608
- console.log(`Bubble at [${row}][${col}] was cleared or game ended. No game over check for this bubble needed.`);
609
  }
610
  } else {
611
- console.warn(`Could not place bubble. Slot [${row}][${col}] invalid, out of bounds, or occupied. landedY=${landedBubbleData.y.toFixed(1)}`);
612
  const lowestGridBubbleY = getBubbleY(ROWS -1);
613
  if(landedBubbleData.y > lowestGridBubbleY + BUBBLE_RADIUS * 1.5) {
614
  triggerGameOver("Game Over: Bubble fell off bottom of grid!");
@@ -617,18 +683,11 @@
617
  }
618
  }
619
 
620
- if (currentGameState !== GAME_STATE.PLAYING) {
621
- console.log("Game state is not PLAYING after primary landing logic, returning.");
622
- return;
623
- }
624
 
625
- console.log("Removing floating bubbles after landing logic.");
626
  removeFloatingBubbles();
627
 
628
- if (currentGameState !== GAME_STATE.PLAYING) {
629
- console.log("Game state is not PLAYING after removeFloatingBubbles, returning.");
630
- return;
631
- }
632
 
633
  let bubblesExist = false;
634
  for (let r = 0; r < ROWS; r++) {
@@ -639,21 +698,18 @@
639
  if(bubblesExist) break;
640
  }
641
  if (!bubblesExist) {
642
- console.log("No bubbles exist, triggering You Win.");
643
  triggerYouWin();
644
  return;
645
  }
646
 
647
  if (shotsFiredSinceLastRow >= SHOTS_UNTIL_NEW_ROW_THRESHOLD) {
648
- console.log("Threshold reached, adding new row.");
649
  addNewRow();
650
  shotsFiredSinceLastRow = 0;
651
  } else {
652
- console.log("Threshold not reached OR new row added. Manually calling checkIfGameOver.");
653
  checkIfGameOver();
654
  }
655
  updateShotsDisplay();
656
- console.log("--- handleBubbleLanded() Finished ---");
657
  }
658
 
659
  function findBestGridSlot(x, y) {
@@ -665,7 +721,7 @@
665
  for (let c = 0; c < getColsInRow(r); c++) {
666
  const slotX = getBubbleX(c, r);
667
  const slotY = getBubbleY(r);
668
- const yBias = (ROWS - r) * 0.01 * BUBBLE_RADIUS; // Give slight preference to higher rows
669
  const dist = Math.hypot(x - slotX, y - slotY) - yBias;
670
 
671
  if (dist < minDist && dist < BUBBLE_DIAMETER * 1.6) {
@@ -678,17 +734,16 @@
678
  }
679
  }
680
 
681
- if (bestRow === -1) { // Fallback if no ideal empty slot found
682
- console.warn("findBestGridSlot: No ideal empty slot found, using more direct fallback.");
683
  let r_approx = 0;
684
- const firstRowY = getBubbleY(0); // Y-coordinate of the center of bubbles in the first row
685
- if (y < firstRowY - BUBBLE_RADIUS) { // If clearly above the first row
686
  r_approx = 0;
687
  } else {
688
- // Approximate row based on y-coordinate relative to packed row height
689
  r_approx = Math.floor((y - (firstRowY - BUBBLE_RADIUS)) / (BUBBLE_RADIUS * 2 * 0.866));
690
  }
691
- r_approx = Math.max(0, Math.min(ROWS - 1, r_approx)); // Clamp to valid row indices
692
 
693
  let c_approx = 0;
694
  if (bubbleGrid[r_approx]) {
@@ -696,16 +751,14 @@
696
  c_approx = Math.round((x - BUBBLE_RADIUS - offsetXForRow) / BUBBLE_DIAMETER);
697
  c_approx = Math.max(0, Math.min(getColsInRow(r_approx) - 1, c_approx));
698
  } else {
699
- console.error(`Fallback slot finding: Approximated Row ${r_approx} does not exist in bubbleGrid! THIS IS A BUG.`);
700
  return {row: -1, col: -1};
701
  }
702
 
703
  if (bubbleGrid[r_approx] && !bubbleGrid[r_approx][c_approx]) {
704
  bestRow = r_approx;
705
  bestCol = c_approx;
706
- console.log(`Fallback (direct approximation) found empty slot at [${bestRow}][${bestCol}]`);
707
  } else {
708
- console.warn(`Fallback slot [${r_approx}][${c_approx}] is occupied. Trying neighbors in row ${r_approx}...`);
709
  if (bubbleGrid[r_approx]) {
710
  for (let offset = 1; offset <= Math.max(c_approx, getColsInRow(r_approx) - 1 - c_approx) + 1; offset++) {
711
  const c_left = c_approx - offset;
@@ -717,19 +770,15 @@
717
  bestRow = r_approx; bestCol = c_right; break;
718
  }
719
  }
720
- if (bestRow !== -1) console.log(`Fallback neighbor search found [${bestRow}][${bestCol}]`);
721
- else console.warn("Fallback neighbor search failed to find an empty slot in the approximated row.");
722
  }
723
  }
724
  }
725
- console.log(`findBestGridSlot final decision: r=${bestRow}, c=${bestCol} for input x=${x.toFixed(1)}, y=${y.toFixed(1)}`);
726
  return { row: bestRow, col: bestCol };
727
  }
728
 
729
  function checkMatches(startRow, startCol, color) {
730
  if (currentGameState !== GAME_STATE.PLAYING) return false;
731
  if (!bubbleGrid[startRow] || !bubbleGrid[startRow][startCol] || bubbleGrid[startRow][startCol].powerUpType !== POWERUP_TYPE.NONE) {
732
- // console.log(`checkMatches: Bubble at [${startRow}][${startCol}] is not a normal bubble or doesn't exist. Aborting.`);
733
  return false;
734
  }
735
 
@@ -756,11 +805,11 @@
756
  }
757
 
758
  if (matched.length >= 3) {
759
- console.log(`Found ${matched.length} matches starting from [${startRow}][${startCol}] of color ${color}`);
760
  matched.forEach(b => {
761
  if (bubbleGrid[b.r] && bubbleGrid[b.r][b.c]) {
762
  addFallingBubble(bubbleGrid[b.r][b.c]);
763
- createParticles(bubbleGrid[b.r][b.c].x, bubbleGrid[b.r][b.c].y, bubbleGrid[b.r][b.c].color);
764
  bubbleGrid[b.r][b.c] = null;
765
  score += 10;
766
  }
@@ -768,78 +817,138 @@
768
  updateScoreDisplay();
769
  return true;
770
  }
771
- // console.log(`No significant match (found ${matched.length}) for [${startRow}][${startCol}] color ${color}`);
772
  return false;
773
  }
774
 
775
  function activateRainbow(row, col) {
776
- console.log(`--- activateRainbow at [${row}][${col}] ---`);
777
  if (currentGameState !== GAME_STATE.PLAYING) return;
778
- if (!bubbleGrid[row] || !bubbleGrid[row][col]) {
779
- console.warn("activateRainbow: Rainbow bubble not found at specified location.");
780
- return;
781
- }
782
 
783
- const rainbowBubble = bubbleGrid[row][col];
784
- addFallingBubble(rainbowBubble);
785
- createParticles(rainbowBubble.x, rainbowBubble.y, 'white', 20);
786
  bubbleGrid[row][col] = null;
787
- score += 5;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
788
 
789
  const neighbors = getNeighbors(row, col);
790
- console.log(`Rainbow neighbors:`, neighbors);
791
- let clearedByRainbow = false;
792
-
793
  for (const {r: nr, c: nc} of neighbors) {
794
  if (bubbleGrid[nr] && bubbleGrid[nr][nc] && bubbleGrid[nr][nc].powerUpType === POWERUP_TYPE.NONE) {
795
- const neighborColor = bubbleGrid[nr][nc].color;
796
- console.log(`Checking rainbow effect for neighbor [${nr}][${nc}] color ${neighborColor}`);
797
-
798
- const groupAroundNeighbor = [{r: nr, c: nc}];
799
- const toVisit = [{r: nr, c: nc}];
800
- const visited = new Set();
801
- visited.add(`${nr},${nc}`);
802
-
803
- while(toVisit.length > 0) {
804
- const current = toVisit.pop();
805
- const subNeighbors = getNeighbors(current.r, current.c);
806
- for(const {r: snr, c: snc} of subNeighbors) {
807
- if (snr === row && snc === col) continue;
808
-
809
- if (!visited.has(`${snr},${snc}`) && bubbleGrid[snr] && bubbleGrid[snr][snc] &&
810
- bubbleGrid[snr][snc].color === neighborColor &&
811
- bubbleGrid[snr][snc].powerUpType === POWERUP_TYPE.NONE) {
812
-
813
- visited.add(`${snr},${snc}`);
814
- toVisit.push({r: snr, c: snc});
815
- groupAroundNeighbor.push({r: snr, c: snc});
816
- }
817
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
818
  }
 
 
 
 
 
 
 
 
819
 
820
- if (groupAroundNeighbor.length >= 2) {
821
- console.log(`Rainbow helping clear group of ${groupAroundNeighbor.length} (color ${neighborColor}) starting with neighbor [${nr}][${nc}]`);
822
- groupAroundNeighbor.forEach(b_to_pop => {
823
- if(bubbleGrid[b_to_pop.r] && bubbleGrid[b_to_pop.r][b_to_pop.c]){
824
- addFallingBubble(bubbleGrid[b_to_pop.r][b_to_pop.c]);
825
- createParticles(bubbleGrid[b_to_pop.r][b_to_pop.c].x, bubbleGrid[b_to_pop.r][b_to_pop.c].y, bubbleGrid[b_to_pop.r][b_to_pop.c].color);
826
- bubbleGrid[b_to_pop.r][b_to_pop.c] = null;
827
- score += 10;
828
- clearedByRainbow = true;
829
- }
830
- });
 
 
 
 
 
831
  }
832
  }
833
  }
834
- if(clearedByRainbow) updateScoreDisplay();
835
- console.log("--- activateRainbow finished ---");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
836
  }
837
 
 
838
  function explodeBomb(x, y) {
839
- console.log(`--- explodeBomb at x=${x.toFixed(1)}, y=${y.toFixed(1)} ---`);
840
  if (currentGameState !== GAME_STATE.PLAYING) return;
841
 
842
- createParticles(x, y, '#FFA500', 50);
 
843
  let clearedCount = 0;
844
  for (let r = 0; r < ROWS; r++) {
845
  if (!bubbleGrid[r]) continue;
@@ -847,18 +956,19 @@
847
  if (bubbleGrid[r][c]) {
848
  const dist = Math.hypot(x - bubbleGrid[r][c].x, y - bubbleGrid[r][c].y);
849
  if (dist < BUBBLE_RADIUS * BOMB_RADIUS_MULTIPLIER) {
850
- console.log(`Bomb clearing bubble at [${r}][${c}]`);
851
  addFallingBubble(bubbleGrid[r][c]);
852
- createParticles(bubbleGrid[r][c].x, bubbleGrid[r][c].y, bubbleGrid[r][c].color || '#888888');
 
853
  bubbleGrid[r][c] = null;
854
- score += 15;
855
  clearedCount++;
856
  }
857
  }
858
  }
859
  }
860
  if (clearedCount > 0) updateScoreDisplay();
861
- console.log(`Bomb cleared ${clearedCount} bubbles.`);
862
  }
863
 
864
  function getNeighbors(r, c) {
@@ -885,15 +995,15 @@
885
  ...gridBubble,
886
  x: gridBubble.x,
887
  y: gridBubble.y,
888
- vx: (Math.random() - 0.5) * 2,
889
- vy: -Math.random() * 2 -1,
890
  alpha: 1,
891
  radius: BUBBLE_RADIUS
892
  });
893
  }
894
 
895
  function removeFloatingBubbles() {
896
- console.log("--- removeFloatingBubbles ---");
897
  if (currentGameState !== GAME_STATE.PLAYING) return;
898
 
899
  const connected = new Set();
@@ -909,6 +1019,7 @@
909
  for (let c = 0; c < getColsInRow(r); c++) {
910
  if (bubbleGrid[r][c] && !connected.has(`${r},${c}`)) {
911
  addFallingBubble(bubbleGrid[r][c]);
 
912
  bubbleGrid[r][c] = null;
913
  score += 5;
914
  floatingCleared++;
@@ -916,7 +1027,7 @@
916
  }
917
  }
918
  if (floatingCleared > 0) {
919
- console.log(`Cleared ${floatingCleared} floating bubbles.`);
920
  updateScoreDisplay();
921
  }
922
  }
@@ -931,21 +1042,17 @@
931
  }
932
 
933
  function addNewRow() {
934
- console.log("--- addNewRow ---");
935
- if (currentGameState !== GAME_STATE.PLAYING) {
936
- console.log("addNewRow: Game not playing, aborting.");
937
- return;
938
- }
939
 
940
- // Row *above* the actual game over line
941
  const checkRowForPush = GAME_OVER_ROW_INDEX - 1;
942
- console.log(`addNewRow: Checking for game over push at row ${checkRowForPush} (GAME_OVER_ROW_INDEX is ${GAME_OVER_ROW_INDEX})`);
943
 
944
  if (checkRowForPush >= 0) {
945
  if (bubbleGrid[checkRowForPush]) {
946
  for (let c = 0; c < getColsInRow(checkRowForPush); c++) {
947
  if (bubbleGrid[checkRowForPush][c]) {
948
- console.log(`addNewRow: Bubble found at [${checkRowForPush}][${c}], will cause game over on shift.`);
949
  shiftRowsDown();
950
  triggerGameOver(`Game Over: New row pushed bubbles to game over line ${GAME_OVER_ROW_INDEX}.`);
951
  return;
@@ -954,7 +1061,7 @@
954
  }
955
  }
956
 
957
- console.log("addNewRow: No immediate game over from push. Shifting rows down.");
958
  shiftRowsDown();
959
 
960
  bubbleGrid[0] = new Array(getColsInRow(0)).fill(null);
@@ -963,12 +1070,12 @@
963
  bubbleGrid[0][c] = createGridBubble(c, 0);
964
  }
965
  }
966
- console.log("New top row created.");
967
  checkIfGameOver();
968
  }
969
 
970
  function shiftRowsDown() {
971
- console.log("Shifting rows down...");
972
  for (let r = ROWS - 1; r > 0; r--) {
973
  bubbleGrid[r] = bubbleGrid[r-1];
974
  if (bubbleGrid[r]) {
@@ -985,15 +1092,9 @@
985
  }
986
 
987
  function checkIfGameOver() {
988
- console.log(`--- checkIfGameOver (using GAME_OVER_ROW_INDEX: ${GAME_OVER_ROW_INDEX}) ---`);
989
- if (currentGameState !== GAME_STATE.PLAYING) {
990
- console.log("checkIfGameOver: Game not playing, aborting check.");
991
- return;
992
- }
993
- if (GAME_OVER_ROW_INDEX < 0 || GAME_OVER_ROW_INDEX >= ROWS) {
994
- console.error(`checkIfGameOver: Invalid GAME_OVER_ROW_INDEX (${GAME_OVER_ROW_INDEX}) for ROWS (${ROWS}). Aborting check.`);
995
- return;
996
- }
997
 
998
  if (bubbleGrid[GAME_OVER_ROW_INDEX]) {
999
  for (let c = 0; c < getColsInRow(GAME_OVER_ROW_INDEX); c++) {
@@ -1003,17 +1104,15 @@
1003
  }
1004
  }
1005
  }
1006
- console.log("checkIfGameOver: No bubbles on game over line.");
1007
  }
1008
 
1009
  function updateScoreDisplay() { scoreElement.textContent = score; }
1010
  function updateShotsDisplay() { shotsUntilNextRowElement.textContent = Math.max(0, SHOTS_UNTIL_NEW_ROW_THRESHOLD - shotsFiredSinceLastRow); }
1011
 
1012
  function triggerGameOver(message = "Game Over!") {
1013
- if (currentGameState === GAME_STATE.GAME_OVER || currentGameState === GAME_STATE.YOU_WIN) {
1014
- console.warn(`triggerGameOver called with message "${message}", but game already ended (${currentGameState}). Ignoring.`);
1015
- return;
1016
- }
1017
  currentGameState = GAME_STATE.GAME_OVER;
1018
  overlayTitle.textContent = message;
1019
  overlayScore.textContent = `Final Score: ${score}`;
@@ -1022,15 +1121,13 @@
1022
  }
1023
 
1024
  function triggerYouWin() {
1025
- if (currentGameState === GAME_STATE.YOU_WIN || currentGameState === GAME_STATE.GAME_OVER) {
1026
- console.warn(`triggerYouWin called, but game already ended (${currentGameState}). Ignoring.`);
1027
- return;
1028
- }
1029
  currentGameState = GAME_STATE.YOU_WIN;
1030
  overlayTitle.textContent = "Congratulations! You Win!";
1031
  overlayScore.textContent = `Final Score: ${score}`;
1032
  overlayScreen.style.display = 'flex';
1033
- console.log("%c YOU WIN! ", "background: green; color: white; font-size: 20px;", `Score: ${score}`);
1034
  }
1035
 
1036
  function handleMouseDown(e) {
@@ -1043,14 +1140,12 @@
1043
  pos.y >= area.y && pos.y <= area.y + area.height) {
1044
  if (shooter.currentBubble && shooter.nextBubble) {
1045
  [shooter.currentBubble, shooter.nextBubble] = [shooter.nextBubble, shooter.currentBubble];
1046
- console.log("Bubbles swapped.");
1047
  }
1048
  return;
1049
  }
1050
 
1051
  isAiming = true;
1052
- aimStartX = shooter.x;
1053
- aimStartY = shooter.y;
1054
  updateAimAngle(pos.x, pos.y);
1055
  }
1056
 
@@ -1092,8 +1187,8 @@
1092
  let dy = mouseY - shooter.y;
1093
  shooter.angle = Math.atan2(dy, dx);
1094
 
1095
- const minAngle = -Math.PI * 0.95;
1096
- const maxAngle = -Math.PI * 0.05;
1097
  shooter.angle = Math.max(minAngle, Math.min(shooter.angle, maxAngle));
1098
  }
1099
 
@@ -1107,7 +1202,7 @@
1107
  updateMovingBubble();
1108
 
1109
  drawGrid();
1110
- drawShooter();
1111
  drawMovingBubble();
1112
  drawFallingBubbles();
1113
  drawParticles();
@@ -1122,28 +1217,25 @@
1122
  requestAnimationFrame(gameLoop);
1123
  }
1124
 
1125
- // Event Listeners for Buttons
1126
  startButton.addEventListener('click', () => {
1127
- console.log("Start button clicked.");
1128
- setupCanvas(); // CRITICAL: Setup dimensions and GAME_OVER_ROW_INDEX first
1129
- initGame(); // Then initialize game logic using those dimensions
1130
  });
1131
 
1132
  restartButton.addEventListener('click', () => {
1133
- console.log("Restart button clicked.");
1134
- // It's good practice to re-setup canvas in case of resize, though less critical if game is fixed size once started
1135
  setupCanvas();
1136
  initGame();
1137
  });
1138
 
1139
  menuButton.addEventListener('click', () => {
1140
- console.log("Menu button clicked.");
1141
  currentGameState = GAME_STATE.MENU;
1142
  overlayScreen.style.display = 'none';
1143
  startScreen.style.display = 'flex';
1144
  });
1145
 
1146
- // Touch/Mouse event listeners for canvas
1147
  canvas.addEventListener('mousedown', handleMouseDown);
1148
  canvas.addEventListener('mousemove', handleMouseMove);
1149
  canvas.addEventListener('mouseup', handleMouseUp);
@@ -1151,10 +1243,9 @@
1151
  canvas.addEventListener('touchmove', handleMouseMove, { passive: false });
1152
  canvas.addEventListener('touchend', handleMouseUp, { passive: false });
1153
 
1154
- // Initial call when the page loads
1155
- console.log("Initial page load: Setting up canvas for MENU state display and starting game loop.");
1156
- setupCanvas(); // Setup initial canvas dimensions for the menu screen.
1157
- gameLoop(); // Start the game loop, which will initially just show the menu (or a blank canvas if menu is pure HTML).
1158
  </script>
1159
  </body>
1160
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
+ <title>Enhanced Bubble Shooter Pro</title>
7
  <style>
8
  body {
9
  margin: 0;
 
14
  align-items: center;
15
  min-height: 100vh;
16
  font-family: 'Arial', sans-serif;
17
+ overflow: hidden;
18
+ touch-action: none;
19
  }
20
 
21
  .game-wrapper {
22
+ position: relative;
23
  }
24
 
25
  .game-container {
26
  background: rgba(255, 255, 255, 0.1);
27
  border-radius: 20px;
28
+ padding: 15px;
29
  backdrop-filter: blur(10px);
30
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
31
  text-align: center;
32
+ width: fit-content;
33
  }
34
 
35
  canvas {
36
  border: 2px solid #fff;
37
  border-radius: 10px;
38
+ background: #000022;
39
  display: block;
40
  margin: 0 auto;
41
  }
 
46
  display: flex;
47
  justify-content: space-around;
48
  align-items: center;
49
+ font-size: 16px;
50
  }
51
 
52
  .score, .shots-info {
 
70
  font-weight: bold;
71
  text-align: center;
72
  z-index: 100;
73
+ border-radius: 10px;
74
  }
75
  .overlay-message h2 {
76
  margin-bottom: 20px;
77
  font-size: 1.5em;
78
  }
79
 
80
+ .controls-info {
81
  margin-top: 10px;
82
  color: rgba(255, 255, 255, 0.85);
83
+ font-size: 12px;
84
  }
85
 
86
  button.game-button {
 
102
  button.game-button:active {
103
  transform: scale(0.95);
104
  }
 
 
105
  .next-bubble-display {
106
  cursor: pointer;
107
  padding: 5px;
 
121
  <div class="score">Score: <span id="score">0</span></div>
122
  <div class="shots-info">Next Row: <span id="shotsUntilNextRow">0</span></div>
123
  </div>
124
+ <canvas id="gameCanvas"></canvas>
125
  <div class="controls-info">
126
  Aim & Shoot: Drag from shooter base / Click<br>
127
  Connect 3+ same colors. Tap "Next" to swap.
 
135
  <button id="menuButton" class="game-button">Main Menu</button>
136
  </div>
137
  <div id="startScreen" class="overlay-message">
138
+ <h2>Bubble Shooter Pro</h2>
139
  <p>Ready to pop some bubbles?</p>
140
  <button id="startButton" class="game-button">Start Game</button>
141
  </div>
 
156
  const menuButton = document.getElementById('menuButton');
157
  const startButton = document.getElementById('startButton');
158
 
 
159
  const GAME_STATE = {
160
  MENU: 'MENU',
161
  PLAYING: 'PLAYING',
 
164
  };
165
  let currentGameState = GAME_STATE.MENU;
166
 
 
167
  let BUBBLE_RADIUS = 0;
168
  let BUBBLE_DIAMETER = 0;
169
  let ROWS = 0;
170
+ let COLS = 12;
171
  let GAME_OVER_ROW_INDEX = 0;
172
 
173
  const INIT_ROWS_COUNT_TARGET = 5;
174
  const SHOTS_UNTIL_NEW_ROW_THRESHOLD = 6;
175
+ const MAX_TRAJECTORY_BOUNCES = 2; // For aiming line
176
 
177
  const BUBBLE_COLORS = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3', '#54a0ff'];
178
 
179
  const POWERUP_TYPE = {
180
  NONE: 'NONE',
181
  BOMB: 'BOMB',
182
+ RAINBOW: 'RAINBOW',
183
+ COLOR_SPLASH: 'COLOR_SPLASH' // New power-up
184
  };
185
+ const BOMB_RADIUS_MULTIPLIER = 3.0; // Slightly larger bomb effect
186
+ const POWERUP_CHANCE = 0.12; // Overall chance for any power-up
187
+ const COLOR_SPLASH_CHANCE_OF_POWERUP = 0.25; // If powerup, 25% chance it's Color Splash
188
 
 
189
  let score = 0;
190
  let shotsFiredSinceLastRow = 0;
191
  const bubbleGrid = [];
 
195
  let particles = [];
196
 
197
  let isAiming = false;
 
 
198
 
199
  function setupCanvas() {
200
+ // console.log("--- Running setupCanvas ---");
201
  const screenWidth = window.innerWidth;
202
  const screenHeight = window.innerHeight;
 
203
  const maxCanvasWidth = Math.min(screenWidth * 0.95, 500);
204
 
 
205
  BUBBLE_RADIUS = Math.floor(maxCanvasWidth / ( (COLS + 0.5) * 2));
206
  BUBBLE_DIAMETER = BUBBLE_RADIUS * 2;
 
207
 
208
  canvas.width = Math.floor((COLS + 0.5) * BUBBLE_DIAMETER);
209
 
210
  const bubbleRowHeight = BUBBLE_RADIUS * 2 * 0.866;
211
+ const availableGridHeight = Math.min(screenHeight * 0.70, canvas.width * 1.3);
 
212
  ROWS = Math.floor(availableGridHeight / bubbleRowHeight);
213
+ ROWS = Math.max(ROWS, 10);
214
 
 
215
  canvas.height = Math.floor(ROWS * bubbleRowHeight + BUBBLE_DIAMETER * 3.5);
 
 
216
  GAME_OVER_ROW_INDEX = ROWS - 1;
217
+ // console.log(`Canvas W: ${canvas.width}, H: ${canvas.height}. COLS: ${COLS}, ROWS: ${ROWS}. GAME_OVER_ROW_INDEX: ${GAME_OVER_ROW_INDEX}`);
218
 
219
  shooter.x = canvas.width / 2;
220
+ shooter.y = canvas.height - BUBBLE_DIAMETER * 1.8;
221
 
222
  shooter.nextBubbleArea = {
223
  x: shooter.x + BUBBLE_DIAMETER * 1.5,
 
225
  width: BUBBLE_DIAMETER * 1.2,
226
  height: BUBBLE_DIAMETER * 1.2
227
  };
228
+ // console.log("--- setupCanvas Finished ---");
229
  }
230
 
231
  function initGame() {
232
+ // console.log("--- Running initGame ---");
 
 
233
  currentGameState = GAME_STATE.PLAYING;
234
  overlayScreen.style.display = 'none';
235
  startScreen.style.display = 'none';
 
240
  fallingBubbles = [];
241
  particles = [];
242
 
243
+ bubbleGrid.length = 0;
244
  for (let r = 0; r < ROWS; r++) {
245
  bubbleGrid[r] = new Array(getColsInRow(r)).fill(null);
246
  }
247
 
 
 
248
  let initRowsToFill = Math.min(INIT_ROWS_COUNT_TARGET, ROWS - 5);
249
+ initRowsToFill = Math.max(1, initRowsToFill);
250
 
251
+ // console.log(`Initializing game grid: Total ROWS=${ROWS}, initRowsToFill=${initRowsToFill}, GAME_OVER_ROW_INDEX=${GAME_OVER_ROW_INDEX}`);
252
 
253
+ if (initRowsToFill >= GAME_OVER_ROW_INDEX && GAME_OVER_ROW_INDEX > 0 ) {
254
+ // console.error(`CRITICAL: initRowsToFill (${initRowsToFill}) is too close to or exceeds GAME_OVER_ROW_INDEX (${GAME_OVER_ROW_INDEX}). Reducing initRowsToFill.`);
255
+ initRowsToFill = Math.max(1, GAME_OVER_ROW_INDEX - 2);
256
  }
257
+ if (initRowsToFill < 1 && ROWS > 0) initRowsToFill = 1;
258
+ else if (ROWS === 0) {
259
+ // console.error("CRITICAL: ROWS is 0. Cannot initialize game grid.");
260
+ triggerGameOver("Error: Game grid could not be initialized.");
 
 
 
 
261
  return;
262
  }
263
 
 
264
  for (let r = 0; r < initRowsToFill; r++) {
265
  for (let c = 0; c < getColsInRow(r); c++) {
266
  if (Math.random() < 0.7) {
 
273
  shooter.nextBubble = createShooterBubble();
274
  updateScoreDisplay();
275
  updateShotsDisplay();
276
+ // console.log("--- initGame Finished ---");
277
  }
278
 
279
  function getColsInRow(row) {
 
286
  }
287
 
288
  function getBubbleY(row) {
289
+ const gridTopMargin = BUBBLE_RADIUS * 0.5;
290
  return row * (BUBBLE_RADIUS * 2 * 0.866) + BUBBLE_RADIUS + gridTopMargin;
291
  }
292
 
 
303
 
304
  function createShooterBubble() {
305
  const existingColors = new Set();
306
+ let normalBubblesExist = false;
307
  for (let r = 0; r < ROWS; r++) {
308
  if (!bubbleGrid[r]) continue;
309
  for (let c = 0; c < getColsInRow(r); c++) {
310
  if (bubbleGrid[r][c] && bubbleGrid[r][c].powerUpType === POWERUP_TYPE.NONE) {
311
  existingColors.add(bubbleGrid[r][c].color);
312
+ normalBubblesExist = true;
313
  }
314
  }
315
  }
316
  let availableColors = Array.from(existingColors);
317
+ if (!normalBubblesExist || availableColors.length === 0) { // If only powerups left or grid is empty
318
+ availableColors = [...BUBBLE_COLORS]; // Fallback to all colors
319
+ }
320
 
321
  let powerUp = POWERUP_TYPE.NONE;
322
  let color = availableColors[Math.floor(Math.random() * availableColors.length)];
323
 
324
  if (Math.random() < POWERUP_CHANCE) {
325
+ const randPowerUpType = Math.random();
326
+ if (randPowerUpType < COLOR_SPLASH_CHANCE_OF_POWERUP) {
327
+ powerUp = POWERUP_TYPE.COLOR_SPLASH;
328
+ color = '#DDA0DD'; // Plum color for Color Splash
329
+ } else if (randPowerUpType < COLOR_SPLASH_CHANCE_OF_POWERUP + 0.4) { // Bomb is common
330
  powerUp = POWERUP_TYPE.BOMB;
331
  color = '#333333';
332
+ } else { // Rainbow fills the rest
333
  powerUp = POWERUP_TYPE.RAINBOW;
334
+ // Rainbow color is dynamic
335
  }
336
  }
337
 
 
344
 
345
  function drawBubble(bubble) {
346
  const { x, y, color, radius, powerUpType } = bubble;
347
+ if (!radius || radius <= 0) return;
348
+
 
 
349
  ctx.beginPath();
350
  ctx.arc(x, y, radius, 0, Math.PI * 2);
351
 
352
  if (powerUpType === POWERUP_TYPE.RAINBOW) {
353
+ const rainbowGradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
354
+ BUBBLE_COLORS.forEach((c, i) => rainbowGradient.addColorStop(Math.min(0.9, i / BUBBLE_COLORS.length), c)); // Cap stops for better blend
355
+ rainbowGradient.addColorStop(1, BUBBLE_COLORS[0]); // Ensure full circle
356
  ctx.fillStyle = rainbowGradient;
357
+ } else if (powerUpType === POWERUP_TYPE.COLOR_SPLASH) {
358
+ const splashGradient = ctx.createRadialGradient(x,y,radius*0.2, x,y,radius);
359
+ splashGradient.addColorStop(0, 'white');
360
+ splashGradient.addColorStop(0.3, color); // Its base color (e.g., Plum)
361
+ splashGradient.addColorStop(0.6, BUBBLE_COLORS[Math.floor(Date.now()/500) % BUBBLE_COLORS.length]); // Cycling color
362
+ splashGradient.addColorStop(1, color);
363
+ ctx.fillStyle = splashGradient;
364
+ }
365
+ else {
366
  ctx.fillStyle = color;
367
  }
368
  ctx.fill();
369
 
370
+ // Shine effect
371
+ const shineGradient = ctx.createRadialGradient(x - radius*0.35, y - radius*0.35, radius*0.1, x - radius*0.1, y - radius*0.1, radius*0.8);
372
+ shineGradient.addColorStop(0, 'rgba(255, 255, 255, 0.6)');
373
+ shineGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
374
+ ctx.fillStyle = shineGradient;
375
  ctx.fill();
376
 
377
+ // Icons for power-ups
378
+ ctx.fillStyle = 'white';
379
+ ctx.textAlign = 'center';
380
+ ctx.textBaseline = 'middle';
381
  if (powerUpType === POWERUP_TYPE.BOMB) {
382
+ ctx.font = `bold ${radius * 0.9}px Arial`;
383
+ ctx.fillText('💣', x, y + radius*0.1);
384
+ } else if (powerUpType === POWERUP_TYPE.COLOR_SPLASH) {
385
+ ctx.font = `bold ${radius * 0.8}px Arial`;
386
+ ctx.fillText('💦', x, y + radius*0.1); // Splash icon
387
  }
388
 
389
  ctx.strokeStyle = 'rgba(0,0,0,0.15)';
 
391
  ctx.stroke();
392
  }
393
 
394
+ function drawTrajectory() {
395
+ if (!shooter.currentBubble || movingBubble) return;
396
+
397
+ ctx.beginPath();
398
+ ctx.setLineDash([BUBBLE_RADIUS/3, BUBBLE_RADIUS/3]);
399
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
400
+ ctx.lineWidth = 2;
401
+
402
+ let currentX = shooter.x;
403
+ let currentY = shooter.y;
404
+ let currentAngle = shooter.angle;
405
+ let bounces = 0;
406
+ const step = 5; // Small steps for trajectory line
407
+
408
+ ctx.moveTo(currentX, currentY);
409
+
410
+ for (let i = 0; i < 200; i++) { // Max trajectory length (number of steps)
411
+ currentX += Math.cos(currentAngle) * step;
412
+ currentY += Math.sin(currentAngle) * step;
413
+
414
+ // Wall collision
415
+ if (currentX <= BUBBLE_RADIUS || currentX >= canvas.width - BUBBLE_RADIUS) {
416
+ if (bounces < MAX_TRAJECTORY_BOUNCES) {
417
+ currentAngle = Math.PI - currentAngle; // Reflect angle
418
+ // Adjust position slightly to prevent getting stuck in wall visually
419
+ currentX = (currentX <= BUBBLE_RADIUS) ? BUBBLE_RADIUS + 1 : canvas.width - BUBBLE_RADIUS - 1;
420
+ bounces++;
421
+ ctx.lineTo(currentX, currentY); // Draw line to the bounce point
422
+ ctx.stroke(); // Stroke the segment before bounce
423
+ ctx.beginPath(); // Start new segment after bounce
424
+ ctx.moveTo(currentX, currentY);
425
+ } else {
426
+ // Max bounces reached, stop drawing trajectory
427
+ ctx.lineTo(currentX, currentY);
428
+ break;
429
+ }
430
+ }
431
+
432
+ // Top collision
433
+ if (currentY <= BUBBLE_RADIUS) {
434
+ currentY = BUBBLE_RADIUS;
435
+ ctx.lineTo(currentX, currentY);
436
+ break;
437
+ }
438
+
439
+ // Check for collision with existing grid bubbles (simplified for trajectory)
440
+ let hitGridBubble = false;
441
+ for (let r = 0; r < ROWS; r++) {
442
+ if (!bubbleGrid[r]) continue;
443
+ for (let c = 0; c < getColsInRow(r); c++) {
444
+ if (bubbleGrid[r][c]) {
445
+ const dist = Math.hypot(currentX - bubbleGrid[r][c].x, currentY - bubbleGrid[r][c].y);
446
+ if (dist < BUBBLE_DIAMETER * 0.9) { // Collision with existing bubble
447
+ hitGridBubble = true;
448
+ break;
449
+ }
450
+ }
451
+ }
452
+ if (hitGridBubble) break;
453
+ }
454
+
455
+ if (hitGridBubble) {
456
+ ctx.lineTo(currentX, currentY);
457
+ break;
458
+ }
459
+
460
+ if (i % 2 === 0) { // Draw line in segments for dashed effect if setLineDash isn't perfect
461
+ ctx.lineTo(currentX, currentY);
462
+ } else {
463
+ ctx.moveTo(currentX, currentY);
464
+ }
465
+
466
+
467
+ }
468
+ ctx.stroke(); // Stroke the final segment
469
+
470
+ // Draw a small circle at the predicted landing spot (end of trajectory)
471
+ ctx.beginPath();
472
+ ctx.arc(currentX, currentY, BUBBLE_RADIUS * 0.3, 0, Math.PI * 2);
473
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
474
+ ctx.fill();
475
+
476
+ ctx.setLineDash([]); // Reset line dash
477
+ }
478
+
479
+
480
  function drawGrid() {
481
  for (let r = 0; r < ROWS; r++) {
482
  if (!bubbleGrid[r]) continue;
 
489
  }
490
 
491
  function drawShooter() {
492
+ // Shooter Base
493
  ctx.fillStyle = '#444';
494
  ctx.beginPath();
495
  ctx.arc(shooter.x, shooter.y, BUBBLE_RADIUS * 1.2, Math.PI, 0);
496
  ctx.closePath();
497
  ctx.fill();
498
+ ctx.strokeStyle = '#222';
 
499
  ctx.lineWidth = 2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  ctx.stroke();
501
+
502
+ // Draw enhanced trajectory line
503
+ drawTrajectory();
504
 
505
  if (shooter.currentBubble) {
506
  drawBubble({ ...shooter.currentBubble, x: shooter.x, y: shooter.y });
 
562
  }
563
  }
564
 
565
+ function createParticles(x, y, color, count = 10, speed = 4, sizeRange = [2,5]) {
566
  for (let i = 0; i < count; i++) {
567
  particles.push({
568
  x, y, color,
569
+ vx: (Math.random() - 0.5) * speed,
570
+ vy: (Math.random() - 0.5) * speed,
571
+ size: Math.random() * (sizeRange[1] - sizeRange[0]) + sizeRange[0],
572
+ life: Math.random() * 30 + 30, // Duration in frames
573
  alpha: 1
574
  });
575
  }
576
  }
577
 
578
  function shoot() {
579
+ // console.log("--- shoot() called ---");
580
  if (currentGameState !== GAME_STATE.PLAYING || !shooter.currentBubble || movingBubble) {
581
+ // console.log(`shoot() aborted: gameState=${currentGameState}, currentBubble=${!!shooter.currentBubble}, movingBubble=${!!movingBubble}`);
582
  return;
583
  }
584
 
 
586
  ...shooter.currentBubble,
587
  x: shooter.x,
588
  y: shooter.y,
589
+ vx: Math.cos(shooter.angle) * (BUBBLE_DIAMETER * 0.7), // Slightly faster speed
590
+ vy: Math.sin(shooter.angle) * (BUBBLE_DIAMETER * 0.7)
591
  };
592
+ // console.log("Moving bubble created:", movingBubble);
593
 
594
  shooter.currentBubble = shooter.nextBubble;
595
  shooter.nextBubble = createShooterBubble();
 
611
  }
612
 
613
  if (movingBubble.y <= BUBBLE_RADIUS) {
614
+ // console.log("Moving bubble hit top of screen.");
615
+ movingBubble.y = BUBBLE_RADIUS;
616
  handleBubbleLanded();
617
  return;
618
  }
 
621
  if (!bubbleGrid[r]) continue;
622
  for (let c = 0; c < getColsInRow(r); c++) {
623
  if (bubbleGrid[r][c]) {
624
+ const bubble = bubbleGrid[r][c];
625
  const dist = Math.hypot(movingBubble.x - bubble.x, movingBubble.y - bubble.y);
626
+ if (dist < BUBBLE_DIAMETER * 0.95) {
627
+ // console.log(`Moving bubble collided with grid bubble at [${r}][${c}]`);
628
  handleBubbleLanded();
629
  return;
630
  }
 
634
  }
635
 
636
  function handleBubbleLanded() {
637
+ // console.log("--- handleBubbleLanded() ---");
638
  if (currentGameState !== GAME_STATE.PLAYING) {
 
639
  if (movingBubble) movingBubble = null;
640
  return;
641
  }
642
+ if(!movingBubble) return;
 
 
 
643
 
644
  const landedBubbleData = { ...movingBubble };
645
  movingBubble = null;
646
 
647
+ // console.log(`Landed bubble data: x=${landedBubbleData.x.toFixed(1)}, y=${landedBubbleData.y.toFixed(1)}, type=${landedBubbleData.powerUpType}, color=${landedBubbleData.color}`);
648
+ // console.log(`Current GAME_OVER_ROW_INDEX for checks: ${GAME_OVER_ROW_INDEX}`);
 
649
 
650
  if (landedBubbleData.powerUpType === POWERUP_TYPE.BOMB) {
 
651
  explodeBomb(landedBubbleData.x, landedBubbleData.y);
652
+ } else if (landedBubbleData.powerUpType === POWERUP_TYPE.COLOR_SPLASH) {
653
+ activateColorSplash(landedBubbleData.x, landedBubbleData.y);
654
+ }
655
+ else { // Normal or Rainbow
656
  const { row, col } = findBestGridSlot(landedBubbleData.x, landedBubbleData.y);
657
+ // console.log(`findBestGridSlot returned: target row=${row}, col=${col}.`);
658
 
659
  if (row !== -1 && col !== -1 && row < ROWS && col < getColsInRow(row) && bubbleGrid[row] && (bubbleGrid[row][col] === null || bubbleGrid[row][col] === undefined) ) {
660
  bubbleGrid[row][col] = createGridBubble(col, row, landedBubbleData.color, landedBubbleData.powerUpType);
661
+ // console.log(`Placed bubble at [${row}][${col}]. Color: ${bubbleGrid[row][col].color}, Type: ${bubbleGrid[row][col].powerUpType}`);
662
 
663
  if (landedBubbleData.powerUpType === POWERUP_TYPE.RAINBOW) {
 
664
  activateRainbow(row, col);
665
  } else {
 
666
  checkMatches(row, col, landedBubbleData.color);
667
  }
668
 
669
  if (currentGameState === GAME_STATE.PLAYING && bubbleGrid[row] && bubbleGrid[row][col]) {
670
+ // console.log(`Bubble at [${row}][${col}] still exists. Game Over Check: landed row (${row}) >= GAME_OVER_ROW_INDEX (${GAME_OVER_ROW_INDEX})`);
671
  if (row >= GAME_OVER_ROW_INDEX) {
672
  triggerGameOver(`Game Over: Bubble landed at row ${row}, which is >= game over line ${GAME_OVER_ROW_INDEX}.`);
673
  return;
674
  }
 
 
675
  }
676
  } else {
677
+ // console.warn(`Could not place bubble. Slot [${row}][${col}] invalid or occupied. landedY=${landedBubbleData.y.toFixed(1)}`);
678
  const lowestGridBubbleY = getBubbleY(ROWS -1);
679
  if(landedBubbleData.y > lowestGridBubbleY + BUBBLE_RADIUS * 1.5) {
680
  triggerGameOver("Game Over: Bubble fell off bottom of grid!");
 
683
  }
684
  }
685
 
686
+ if (currentGameState !== GAME_STATE.PLAYING) return;
 
 
 
687
 
 
688
  removeFloatingBubbles();
689
 
690
+ if (currentGameState !== GAME_STATE.PLAYING) return;
 
 
 
691
 
692
  let bubblesExist = false;
693
  for (let r = 0; r < ROWS; r++) {
 
698
  if(bubblesExist) break;
699
  }
700
  if (!bubblesExist) {
 
701
  triggerYouWin();
702
  return;
703
  }
704
 
705
  if (shotsFiredSinceLastRow >= SHOTS_UNTIL_NEW_ROW_THRESHOLD) {
 
706
  addNewRow();
707
  shotsFiredSinceLastRow = 0;
708
  } else {
 
709
  checkIfGameOver();
710
  }
711
  updateShotsDisplay();
712
+ // console.log("--- handleBubbleLanded() Finished ---");
713
  }
714
 
715
  function findBestGridSlot(x, y) {
 
721
  for (let c = 0; c < getColsInRow(r); c++) {
722
  const slotX = getBubbleX(c, r);
723
  const slotY = getBubbleY(r);
724
+ const yBias = (ROWS - r) * 0.01 * BUBBLE_RADIUS;
725
  const dist = Math.hypot(x - slotX, y - slotY) - yBias;
726
 
727
  if (dist < minDist && dist < BUBBLE_DIAMETER * 1.6) {
 
734
  }
735
  }
736
 
737
+ if (bestRow === -1) {
738
+ // console.warn("findBestGridSlot: No ideal empty slot found, using more direct fallback.");
739
  let r_approx = 0;
740
+ const firstRowY = getBubbleY(0);
741
+ if (y < firstRowY - BUBBLE_RADIUS) {
742
  r_approx = 0;
743
  } else {
 
744
  r_approx = Math.floor((y - (firstRowY - BUBBLE_RADIUS)) / (BUBBLE_RADIUS * 2 * 0.866));
745
  }
746
+ r_approx = Math.max(0, Math.min(ROWS - 1, r_approx));
747
 
748
  let c_approx = 0;
749
  if (bubbleGrid[r_approx]) {
 
751
  c_approx = Math.round((x - BUBBLE_RADIUS - offsetXForRow) / BUBBLE_DIAMETER);
752
  c_approx = Math.max(0, Math.min(getColsInRow(r_approx) - 1, c_approx));
753
  } else {
754
+ // console.error(`Fallback slot finding: Approximated Row ${r_approx} does not exist in bubbleGrid! THIS IS A BUG.`);
755
  return {row: -1, col: -1};
756
  }
757
 
758
  if (bubbleGrid[r_approx] && !bubbleGrid[r_approx][c_approx]) {
759
  bestRow = r_approx;
760
  bestCol = c_approx;
 
761
  } else {
 
762
  if (bubbleGrid[r_approx]) {
763
  for (let offset = 1; offset <= Math.max(c_approx, getColsInRow(r_approx) - 1 - c_approx) + 1; offset++) {
764
  const c_left = c_approx - offset;
 
770
  bestRow = r_approx; bestCol = c_right; break;
771
  }
772
  }
 
 
773
  }
774
  }
775
  }
 
776
  return { row: bestRow, col: bestCol };
777
  }
778
 
779
  function checkMatches(startRow, startCol, color) {
780
  if (currentGameState !== GAME_STATE.PLAYING) return false;
781
  if (!bubbleGrid[startRow] || !bubbleGrid[startRow][startCol] || bubbleGrid[startRow][startCol].powerUpType !== POWERUP_TYPE.NONE) {
 
782
  return false;
783
  }
784
 
 
805
  }
806
 
807
  if (matched.length >= 3) {
808
+ // console.log(`Found ${matched.length} matches starting from [${startRow}][${startCol}] of color ${color}`);
809
  matched.forEach(b => {
810
  if (bubbleGrid[b.r] && bubbleGrid[b.r][b.c]) {
811
  addFallingBubble(bubbleGrid[b.r][b.c]);
812
+ createParticles(bubbleGrid[b.r][b.c].x, bubbleGrid[b.r][b.c].y, bubbleGrid[b.r][b.c].color, 15, 5, [2,4]);
813
  bubbleGrid[b.r][b.c] = null;
814
  score += 10;
815
  }
 
817
  updateScoreDisplay();
818
  return true;
819
  }
 
820
  return false;
821
  }
822
 
823
  function activateRainbow(row, col) {
824
+ // console.log(`--- activateRainbow at [${row}][${col}] ---`);
825
  if (currentGameState !== GAME_STATE.PLAYING) return;
826
+ if (!bubbleGrid[row] || !bubbleGrid[row][col]) return;
 
 
 
827
 
828
+ const rainbowBubbleOriginal = bubbleGrid[row][col];
829
+ addFallingBubble(rainbowBubbleOriginal);
830
+ createParticles(rainbowBubbleOriginal.x, rainbowBubbleOriginal.y, 'white', 30, 6, [3,6]);
831
  bubbleGrid[row][col] = null;
832
+ score += 20; // More points for rainbow
833
+
834
+ // Determine target color: use next shooter bubble's color if it's normal
835
+ let targetColor = null;
836
+ if (shooter.nextBubble && shooter.nextBubble.powerUpType === POWERUP_TYPE.NONE) {
837
+ targetColor = shooter.nextBubble.color;
838
+ } else { // Fallback: find a common color on the board
839
+ const colorCounts = {};
840
+ let maxCount = 0;
841
+ for (let r_scan = 0; r_scan < ROWS; r_scan++) {
842
+ if (!bubbleGrid[r_scan]) continue;
843
+ for (let c_scan = 0; c_scan < getColsInRow(r_scan); c_scan++) {
844
+ if (bubbleGrid[r_scan][c_scan] && bubbleGrid[r_scan][c_scan].powerUpType === POWERUP_TYPE.NONE) {
845
+ const clr = bubbleGrid[r_scan][c_scan].color;
846
+ colorCounts[clr] = (colorCounts[clr] || 0) + 1;
847
+ if (colorCounts[clr] > maxCount) {
848
+ maxCount = colorCounts[clr];
849
+ targetColor = clr;
850
+ }
851
+ }
852
+ }
853
+ }
854
+ if (!targetColor) targetColor = BUBBLE_COLORS[0]; // Absolute fallback
855
+ }
856
+ // console.log("Rainbow target color:", targetColor);
857
 
858
  const neighbors = getNeighbors(row, col);
859
+ let changedCount = 0;
 
 
860
  for (const {r: nr, c: nc} of neighbors) {
861
  if (bubbleGrid[nr] && bubbleGrid[nr][nc] && bubbleGrid[nr][nc].powerUpType === POWERUP_TYPE.NONE) {
862
+ bubbleGrid[nr][nc].color = targetColor;
863
+ createParticles(bubbleGrid[nr][nc].x, bubbleGrid[nr][nc].y, targetColor, 5, 2, [1,3]);
864
+ changedCount++;
865
+ }
866
+ }
867
+
868
+ // After changing colors, check for new matches around the affected area
869
+ if (changedCount > 0) {
870
+ // Check matches for each neighbor that was changed
871
+ setTimeout(() => { // Slight delay to let color change render
872
+ neighbors.forEach(({r: nr, c: nc}) => {
873
+ if (bubbleGrid[nr] && bubbleGrid[nr][nc] && bubbleGrid[nr][nc].color === targetColor) {
874
+ checkMatches(nr, nc, targetColor);
 
 
 
 
 
 
 
 
 
875
  }
876
+ });
877
+ removeFloatingBubbles(); // Important after potential matches
878
+ }, 50);
879
+ }
880
+ updateScoreDisplay();
881
+ // console.log("--- activateRainbow finished ---");
882
+ }
883
+
884
+ function activateColorSplash(landedX, landedY) {
885
+ // console.log(`--- activateColorSplash at x=${landedX.toFixed(1)}, y=${landedY.toFixed(1)} ---`);
886
+ if (currentGameState !== GAME_STATE.PLAYING) return;
887
+
888
+ createParticles(landedX, landedY, '#DDA0DD', 40, 7, [3,7]); // Plum colored particles
889
+ score += 25; // Points for using Color Splash
890
+
891
+ // Find all unique normal bubble colors on the board
892
+ const currentBoardColors = new Set();
893
+ for (let r = 0; r < ROWS; r++) {
894
+ if (!bubbleGrid[r]) continue;
895
+ for (let c = 0; c < getColsInRow(r); c++) {
896
+ if (bubbleGrid[r][c] && bubbleGrid[r][c].powerUpType === POWERUP_TYPE.NONE) {
897
+ currentBoardColors.add(bubbleGrid[r][c].color);
898
  }
899
+ }
900
+ }
901
+ const colorsArray = Array.from(currentBoardColors);
902
+ if (colorsArray.length < 2) { // Need at least two colors to make a change meaningful
903
+ // console.log("Color Splash: Not enough distinct colors on board to change.");
904
+ updateScoreDisplay();
905
+ return;
906
+ }
907
 
908
+ const colorToRemove = colorsArray[Math.floor(Math.random() * colorsArray.length)];
909
+ let colorToBecome;
910
+ do {
911
+ colorToBecome = colorsArray[Math.floor(Math.random() * colorsArray.length)];
912
+ } while (colorToBecome === colorToRemove && colorsArray.length > 1); // Ensure it changes to a different color if possible
913
+
914
+ // console.log(`Color Splash: Changing ${colorToRemove} to ${colorToBecome}`);
915
+ let changedBubblesCoords = [];
916
+
917
+ for (let r = 0; r < ROWS; r++) {
918
+ if (!bubbleGrid[r]) continue;
919
+ for (let c = 0; c < getColsInRow(r); c++) {
920
+ if (bubbleGrid[r][c] && bubbleGrid[r][c].powerUpType === POWERUP_TYPE.NONE && bubbleGrid[r][c].color === colorToRemove) {
921
+ bubbleGrid[r][c].color = colorToBecome;
922
+ createParticles(bubbleGrid[r][c].x, bubbleGrid[r][c].y, colorToBecome, 3, 2, [1,2]);
923
+ changedBubblesCoords.push({r,c});
924
  }
925
  }
926
  }
927
+
928
+ // After changing colors, check for new matches globally
929
+ if (changedBubblesCoords.length > 0) {
930
+ setTimeout(() => { // Slight delay
931
+ let anyMatches = false;
932
+ // Check matches starting from all changed bubbles
933
+ changedBubblesCoords.forEach(coord => {
934
+ if (bubbleGrid[coord.r] && bubbleGrid[coord.r][coord.c]) { // Check if bubble still exists
935
+ if(checkMatches(coord.r, coord.c, colorToBecome)) anyMatches = true;
936
+ }
937
+ });
938
+ if (anyMatches) removeFloatingBubbles();
939
+ }, 50);
940
+ }
941
+ updateScoreDisplay();
942
+ // console.log("--- activateColorSplash finished ---");
943
  }
944
 
945
+
946
  function explodeBomb(x, y) {
947
+ // console.log(`--- explodeBomb at x=${x.toFixed(1)}, y=${y.toFixed(1)} ---`);
948
  if (currentGameState !== GAME_STATE.PLAYING) return;
949
 
950
+ createParticles(x, y, '#FFA500', 60, 8, [3,8]); // More particles for bomb
951
+ score += 15;
952
  let clearedCount = 0;
953
  for (let r = 0; r < ROWS; r++) {
954
  if (!bubbleGrid[r]) continue;
 
956
  if (bubbleGrid[r][c]) {
957
  const dist = Math.hypot(x - bubbleGrid[r][c].x, y - bubbleGrid[r][c].y);
958
  if (dist < BUBBLE_RADIUS * BOMB_RADIUS_MULTIPLIER) {
959
+ // console.log(`Bomb clearing bubble at [${r}][${c}]`);
960
  addFallingBubble(bubbleGrid[r][c]);
961
+ // Different particle color for bubbles popped by bomb
962
+ createParticles(bubbleGrid[r][c].x, bubbleGrid[r][c].y, bubbleGrid[r][c].color || '#888888', 8, 4, [2,4]);
963
  bubbleGrid[r][c] = null;
964
+ score += 5; // Extra per bubble in bomb
965
  clearedCount++;
966
  }
967
  }
968
  }
969
  }
970
  if (clearedCount > 0) updateScoreDisplay();
971
+ // console.log(`Bomb cleared ${clearedCount} bubbles.`);
972
  }
973
 
974
  function getNeighbors(r, c) {
 
995
  ...gridBubble,
996
  x: gridBubble.x,
997
  y: gridBubble.y,
998
+ vx: (Math.random() - 0.5) * 2.5, // Slightly more horizontal spread
999
+ vy: -Math.random() * 2.5 - 1.5, // Stronger initial pop
1000
  alpha: 1,
1001
  radius: BUBBLE_RADIUS
1002
  });
1003
  }
1004
 
1005
  function removeFloatingBubbles() {
1006
+ // console.log("--- removeFloatingBubbles ---");
1007
  if (currentGameState !== GAME_STATE.PLAYING) return;
1008
 
1009
  const connected = new Set();
 
1019
  for (let c = 0; c < getColsInRow(r); c++) {
1020
  if (bubbleGrid[r][c] && !connected.has(`${r},${c}`)) {
1021
  addFallingBubble(bubbleGrid[r][c]);
1022
+ createParticles(bubbleGrid[r][c].x, bubbleGrid[r][c].y, bubbleGrid[r][c].color, 5,3,[1,3]); // Small pop for floating
1023
  bubbleGrid[r][c] = null;
1024
  score += 5;
1025
  floatingCleared++;
 
1027
  }
1028
  }
1029
  if (floatingCleared > 0) {
1030
+ // console.log(`Cleared ${floatingCleared} floating bubbles.`);
1031
  updateScoreDisplay();
1032
  }
1033
  }
 
1042
  }
1043
 
1044
  function addNewRow() {
1045
+ // console.log("--- addNewRow ---");
1046
+ if (currentGameState !== GAME_STATE.PLAYING) return;
 
 
 
1047
 
 
1048
  const checkRowForPush = GAME_OVER_ROW_INDEX - 1;
1049
+ // console.log(`addNewRow: Checking for game over push at row ${checkRowForPush} (GAME_OVER_ROW_INDEX is ${GAME_OVER_ROW_INDEX})`);
1050
 
1051
  if (checkRowForPush >= 0) {
1052
  if (bubbleGrid[checkRowForPush]) {
1053
  for (let c = 0; c < getColsInRow(checkRowForPush); c++) {
1054
  if (bubbleGrid[checkRowForPush][c]) {
1055
+ // console.log(`addNewRow: Bubble found at [${checkRowForPush}][${c}], will cause game over on shift.`);
1056
  shiftRowsDown();
1057
  triggerGameOver(`Game Over: New row pushed bubbles to game over line ${GAME_OVER_ROW_INDEX}.`);
1058
  return;
 
1061
  }
1062
  }
1063
 
1064
+ // console.log("addNewRow: No immediate game over from push. Shifting rows down.");
1065
  shiftRowsDown();
1066
 
1067
  bubbleGrid[0] = new Array(getColsInRow(0)).fill(null);
 
1070
  bubbleGrid[0][c] = createGridBubble(c, 0);
1071
  }
1072
  }
1073
+ // console.log("New top row created.");
1074
  checkIfGameOver();
1075
  }
1076
 
1077
  function shiftRowsDown() {
1078
+ // console.log("Shifting rows down...");
1079
  for (let r = ROWS - 1; r > 0; r--) {
1080
  bubbleGrid[r] = bubbleGrid[r-1];
1081
  if (bubbleGrid[r]) {
 
1092
  }
1093
 
1094
  function checkIfGameOver() {
1095
+ // console.log(`--- checkIfGameOver (using GAME_OVER_ROW_INDEX: ${GAME_OVER_ROW_INDEX}) ---`);
1096
+ if (currentGameState !== GAME_STATE.PLAYING) return;
1097
+ if (GAME_OVER_ROW_INDEX < 0 || GAME_OVER_ROW_INDEX >= ROWS) return;
 
 
 
 
 
 
1098
 
1099
  if (bubbleGrid[GAME_OVER_ROW_INDEX]) {
1100
  for (let c = 0; c < getColsInRow(GAME_OVER_ROW_INDEX); c++) {
 
1104
  }
1105
  }
1106
  }
1107
+ // console.log("checkIfGameOver: No bubbles on game over line.");
1108
  }
1109
 
1110
  function updateScoreDisplay() { scoreElement.textContent = score; }
1111
  function updateShotsDisplay() { shotsUntilNextRowElement.textContent = Math.max(0, SHOTS_UNTIL_NEW_ROW_THRESHOLD - shotsFiredSinceLastRow); }
1112
 
1113
  function triggerGameOver(message = "Game Over!") {
1114
+ if (currentGameState === GAME_STATE.GAME_OVER || currentGameState === GAME_STATE.YOU_WIN) return;
1115
+
 
 
1116
  currentGameState = GAME_STATE.GAME_OVER;
1117
  overlayTitle.textContent = message;
1118
  overlayScore.textContent = `Final Score: ${score}`;
 
1121
  }
1122
 
1123
  function triggerYouWin() {
1124
+ if (currentGameState === GAME_STATE.YOU_WIN || currentGameState === GAME_STATE.GAME_OVER) return;
1125
+
 
 
1126
  currentGameState = GAME_STATE.YOU_WIN;
1127
  overlayTitle.textContent = "Congratulations! You Win!";
1128
  overlayScore.textContent = `Final Score: ${score}`;
1129
  overlayScreen.style.display = 'flex';
1130
+ // console.log("%c YOU WIN! ", "background: green; color: white; font-size: 20px;", `Score: ${score}`);
1131
  }
1132
 
1133
  function handleMouseDown(e) {
 
1140
  pos.y >= area.y && pos.y <= area.y + area.height) {
1141
  if (shooter.currentBubble && shooter.nextBubble) {
1142
  [shooter.currentBubble, shooter.nextBubble] = [shooter.nextBubble, shooter.currentBubble];
1143
+ // console.log("Bubbles swapped.");
1144
  }
1145
  return;
1146
  }
1147
 
1148
  isAiming = true;
 
 
1149
  updateAimAngle(pos.x, pos.y);
1150
  }
1151
 
 
1187
  let dy = mouseY - shooter.y;
1188
  shooter.angle = Math.atan2(dy, dx);
1189
 
1190
+ const minAngle = -Math.PI * 0.96; // Slightly wider allowed angle
1191
+ const maxAngle = -Math.PI * 0.04;
1192
  shooter.angle = Math.max(minAngle, Math.min(shooter.angle, maxAngle));
1193
  }
1194
 
 
1202
  updateMovingBubble();
1203
 
1204
  drawGrid();
1205
+ drawShooter(); // This now calls drawTrajectory
1206
  drawMovingBubble();
1207
  drawFallingBubbles();
1208
  drawParticles();
 
1217
  requestAnimationFrame(gameLoop);
1218
  }
1219
 
 
1220
  startButton.addEventListener('click', () => {
1221
+ // console.log("Start button clicked.");
1222
+ setupCanvas();
1223
+ initGame();
1224
  });
1225
 
1226
  restartButton.addEventListener('click', () => {
1227
+ // console.log("Restart button clicked.");
 
1228
  setupCanvas();
1229
  initGame();
1230
  });
1231
 
1232
  menuButton.addEventListener('click', () => {
1233
+ // console.log("Menu button clicked.");
1234
  currentGameState = GAME_STATE.MENU;
1235
  overlayScreen.style.display = 'none';
1236
  startScreen.style.display = 'flex';
1237
  });
1238
 
 
1239
  canvas.addEventListener('mousedown', handleMouseDown);
1240
  canvas.addEventListener('mousemove', handleMouseMove);
1241
  canvas.addEventListener('mouseup', handleMouseUp);
 
1243
  canvas.addEventListener('touchmove', handleMouseMove, { passive: false });
1244
  canvas.addEventListener('touchend', handleMouseUp, { passive: false });
1245
 
1246
+ // console.log("Initial page load: Setting up canvas for MENU state display and starting game loop.");
1247
+ setupCanvas();
1248
+ gameLoop();
 
1249
  </script>
1250
  </body>
1251
  </html>