Spaces:
Running
Running
Update index.html
Browse files- 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>
|
| 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;
|
| 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,7 +46,7 @@
|
|
| 46 |
display: flex;
|
| 47 |
justify-content: space-around;
|
| 48 |
align-items: center;
|
| 49 |
-
font-size: 16px;
|
| 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;
|
| 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,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>
|
| 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;
|
| 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 =
|
| 188 |
-
const POWERUP_CHANCE = 0.
|
|
|
|
| 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 |
-
|
| 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);
|
| 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;
|
| 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;
|
| 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);
|
| 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);
|
| 271 |
}
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 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;
|
| 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)
|
|
|
|
|
|
|
| 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
|
| 342 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
|
|
|
| 379 |
ctx.fill();
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
if (powerUpType === POWERUP_TYPE.BOMB) {
|
| 382 |
-
ctx.
|
| 383 |
-
ctx.
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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) *
|
| 496 |
-
vy: (Math.random() - 0.5) *
|
| 497 |
-
size: Math.random() *
|
| 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.
|
| 516 |
-
vy: Math.sin(shooter.angle) * (BUBBLE_DIAMETER * 0.
|
| 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;
|
| 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];
|
| 551 |
const dist = Math.hypot(movingBubble.x - bubble.x, movingBubble.y - bubble.y);
|
| 552 |
-
if (dist < BUBBLE_DIAMETER * 0.95) {
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 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;
|
| 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) {
|
| 682 |
-
console.warn("findBestGridSlot: No ideal empty slot found, using more direct fallback.");
|
| 683 |
let r_approx = 0;
|
| 684 |
-
const firstRowY = getBubbleY(0);
|
| 685 |
-
if (y < firstRowY - BUBBLE_RADIUS) {
|
| 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));
|
| 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 |
-
|
| 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
|
| 784 |
-
addFallingBubble(
|
| 785 |
-
createParticles(
|
| 786 |
bubbleGrid[row][col] = null;
|
| 787 |
-
score +=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 788 |
|
| 789 |
const neighbors = getNeighbors(row, col);
|
| 790 |
-
|
| 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 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 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 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 831 |
}
|
| 832 |
}
|
| 833 |
}
|
| 834 |
-
|
| 835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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',
|
|
|
|
| 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 |
-
|
|
|
|
| 853 |
bubbleGrid[r][c] = null;
|
| 854 |
-
score +=
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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.
|
| 1096 |
-
const maxAngle = -Math.PI * 0.
|
| 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();
|
| 1129 |
-
initGame();
|
| 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
|
| 1155 |
-
|
| 1156 |
-
|
| 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>
|