Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | |
| <title>Enhanced Bubble Shooter Pro</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| font-family: 'Arial', sans-serif; | |
| overflow: hidden; | |
| touch-action: none; | |
| } | |
| .game-wrapper { | |
| position: relative; | |
| } | |
| .game-container { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 20px; | |
| padding: 15px; | |
| backdrop-filter: blur(10px); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
| text-align: center; | |
| width: fit-content; | |
| } | |
| canvas { | |
| border: 2px solid #fff; | |
| border-radius: 10px; | |
| background: #000022; | |
| display: block; | |
| margin: 0 auto; | |
| } | |
| .ui { | |
| color: white; | |
| margin-bottom: 10px; | |
| display: flex; | |
| justify-content: space-around; | |
| align-items: center; | |
| font-size: 16px; | |
| } | |
| .score, .shots-info { | |
| font-weight: bold; | |
| text-shadow: 1px 1px 3px rgba(0,0,0,0.5); | |
| } | |
| .overlay-message { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0,0,0,0.75); | |
| color: white; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| font-size: 24px; | |
| font-weight: bold; | |
| text-align: center; | |
| z-index: 100; | |
| border-radius: 10px; | |
| } | |
| .overlay-message h2 { | |
| margin-bottom: 20px; | |
| font-size: 1.5em; | |
| } | |
| .controls-info { | |
| margin-top: 10px; | |
| color: rgba(255, 255, 255, 0.85); | |
| font-size: 12px; | |
| } | |
| button.game-button { | |
| background-color: #ff6b6b; | |
| color: white; | |
| border: none; | |
| padding: 12px 25px; | |
| border-radius: 8px; | |
| font-size: 18px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.2); | |
| transition: background-color 0.3s ease, transform 0.1s ease; | |
| margin: 10px; | |
| } | |
| button.game-button:hover { | |
| background-color: #ff4757; | |
| } | |
| button.game-button:active { | |
| transform: scale(0.95); | |
| } | |
| .next-bubble-display { | |
| cursor: pointer; | |
| padding: 5px; | |
| border-radius: 5px; | |
| transition: background-color 0.2s; | |
| } | |
| .next-bubble-display:hover { | |
| background-color: rgba(255,255,255,0.1); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="game-wrapper"> | |
| <div class="game-container"> | |
| <div class="ui"> | |
| <div class="score">Score: <span id="score">0</span></div> | |
| <div class="shots-info">Next Row: <span id="shotsUntilNextRow">0</span></div> | |
| </div> | |
| <canvas id="gameCanvas"></canvas> | |
| <div class="controls-info"> | |
| Aim & Shoot: Drag from shooter base / Click<br> | |
| Connect 3+ same colors. Tap "Next" to swap. | |
| </div> | |
| </div> | |
| <div id="overlayScreen" class="overlay-message" style="display: none;"> | |
| <h2 id="overlayTitle">Game Over!</h2> | |
| <p id="overlayScore"></p> | |
| <button id="restartButton" class="game-button">Restart</button> | |
| <button id="menuButton" class="game-button">Main Menu</button> | |
| </div> | |
| <div id="startScreen" class="overlay-message"> | |
| <h2>Bubble Shooter Pro</h2> | |
| <p>Ready to pop some bubbles?</p> | |
| <button id="startButton" class="game-button">Start Game</button> | |
| </div> | |
| </div> | |
| <script> | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const scoreElement = document.getElementById('score'); | |
| const shotsUntilNextRowElement = document.getElementById('shotsUntilNextRow'); | |
| const overlayScreen = document.getElementById('overlayScreen'); | |
| const overlayTitle = document.getElementById('overlayTitle'); | |
| const overlayScore = document.getElementById('overlayScore'); | |
| const startScreen = document.getElementById('startScreen'); | |
| const restartButton = document.getElementById('restartButton'); | |
| const menuButton = document.getElementById('menuButton'); | |
| const startButton = document.getElementById('startButton'); | |
| const GAME_STATE = { | |
| MENU: 'MENU', | |
| PLAYING: 'PLAYING', | |
| GAME_OVER: 'GAME_OVER', | |
| YOU_WIN: 'YOU_WIN' | |
| }; | |
| let currentGameState = GAME_STATE.MENU; | |
| let BUBBLE_RADIUS = 0; | |
| let BUBBLE_DIAMETER = 0; | |
| let ROWS = 0; | |
| let COLS = 12; | |
| let GAME_OVER_ROW_INDEX = 0; | |
| const INIT_ROWS_COUNT_TARGET = 5; | |
| const SHOTS_UNTIL_NEW_ROW_THRESHOLD = 6; | |
| const MAX_TRAJECTORY_BOUNCES = 2; // For aiming line | |
| const BUBBLE_COLORS = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3', '#54a0ff']; | |
| const POWERUP_TYPE = { | |
| NONE: 'NONE', | |
| BOMB: 'BOMB', | |
| RAINBOW: 'RAINBOW', | |
| COLOR_SPLASH: 'COLOR_SPLASH' // New power-up | |
| }; | |
| const BOMB_RADIUS_MULTIPLIER = 3.0; // Slightly larger bomb effect | |
| const POWERUP_CHANCE = 0.12; // Overall chance for any power-up | |
| const COLOR_SPLASH_CHANCE_OF_POWERUP = 0.25; // If powerup, 25% chance it's Color Splash | |
| let score = 0; | |
| let shotsFiredSinceLastRow = 0; | |
| const bubbleGrid = []; | |
| const shooter = { x: 0, y: 0, angle: -Math.PI / 2, currentBubble: null, nextBubble: null }; | |
| let movingBubble = null; | |
| let fallingBubbles = []; | |
| let particles = []; | |
| let isAiming = false; | |
| function setupCanvas() { | |
| // console.log("--- Running setupCanvas ---"); | |
| const screenWidth = window.innerWidth; | |
| const screenHeight = window.innerHeight; | |
| const maxCanvasWidth = Math.min(screenWidth * 0.95, 500); | |
| BUBBLE_RADIUS = Math.floor(maxCanvasWidth / ( (COLS + 0.5) * 2)); | |
| BUBBLE_DIAMETER = BUBBLE_RADIUS * 2; | |
| canvas.width = Math.floor((COLS + 0.5) * BUBBLE_DIAMETER); | |
| const bubbleRowHeight = BUBBLE_RADIUS * 2 * 0.866; | |
| const availableGridHeight = Math.min(screenHeight * 0.70, canvas.width * 1.3); | |
| ROWS = Math.floor(availableGridHeight / bubbleRowHeight); | |
| ROWS = Math.max(ROWS, 10); | |
| canvas.height = Math.floor(ROWS * bubbleRowHeight + BUBBLE_DIAMETER * 3.5); | |
| GAME_OVER_ROW_INDEX = ROWS - 1; | |
| // console.log(`Canvas W: ${canvas.width}, H: ${canvas.height}. COLS: ${COLS}, ROWS: ${ROWS}. GAME_OVER_ROW_INDEX: ${GAME_OVER_ROW_INDEX}`); | |
| shooter.x = canvas.width / 2; | |
| shooter.y = canvas.height - BUBBLE_DIAMETER * 1.8; | |
| shooter.nextBubbleArea = { | |
| x: shooter.x + BUBBLE_DIAMETER * 1.5, | |
| y: shooter.y - BUBBLE_RADIUS, | |
| width: BUBBLE_DIAMETER * 1.2, | |
| height: BUBBLE_DIAMETER * 1.2 | |
| }; | |
| // console.log("--- setupCanvas Finished ---"); | |
| } | |
| function initGame() { | |
| // console.log("--- Running initGame ---"); | |
| currentGameState = GAME_STATE.PLAYING; | |
| overlayScreen.style.display = 'none'; | |
| startScreen.style.display = 'none'; | |
| score = 0; | |
| shotsFiredSinceLastRow = 0; | |
| movingBubble = null; | |
| fallingBubbles = []; | |
| particles = []; | |
| bubbleGrid.length = 0; | |
| for (let r = 0; r < ROWS; r++) { | |
| bubbleGrid[r] = new Array(getColsInRow(r)).fill(null); | |
| } | |
| let initRowsToFill = Math.min(INIT_ROWS_COUNT_TARGET, ROWS - 5); | |
| initRowsToFill = Math.max(1, initRowsToFill); | |
| // console.log(`Initializing game grid: Total ROWS=${ROWS}, initRowsToFill=${initRowsToFill}, GAME_OVER_ROW_INDEX=${GAME_OVER_ROW_INDEX}`); | |
| if (initRowsToFill >= GAME_OVER_ROW_INDEX && GAME_OVER_ROW_INDEX > 0 ) { | |
| // console.error(`CRITICAL: initRowsToFill (${initRowsToFill}) is too close to or exceeds GAME_OVER_ROW_INDEX (${GAME_OVER_ROW_INDEX}). Reducing initRowsToFill.`); | |
| initRowsToFill = Math.max(1, GAME_OVER_ROW_INDEX - 2); | |
| } | |
| if (initRowsToFill < 1 && ROWS > 0) initRowsToFill = 1; | |
| else if (ROWS === 0) { | |
| // console.error("CRITICAL: ROWS is 0. Cannot initialize game grid."); | |
| triggerGameOver("Error: Game grid could not be initialized."); | |
| return; | |
| } | |
| for (let r = 0; r < initRowsToFill; r++) { | |
| for (let c = 0; c < getColsInRow(r); c++) { | |
| if (Math.random() < 0.7) { | |
| bubbleGrid[r][c] = createGridBubble(c, r); | |
| } | |
| } | |
| } | |
| shooter.currentBubble = createShooterBubble(); | |
| shooter.nextBubble = createShooterBubble(); | |
| updateScoreDisplay(); | |
| updateShotsDisplay(); | |
| // console.log("--- initGame Finished ---"); | |
| } | |
| function getColsInRow(row) { | |
| return (row % 2 === 1) ? COLS -1 : COLS; | |
| } | |
| function getBubbleX(col, row) { | |
| const offsetX = (row % 2 === 1) ? BUBBLE_RADIUS : 0; | |
| return col * BUBBLE_DIAMETER + BUBBLE_RADIUS + offsetX; | |
| } | |
| function getBubbleY(row) { | |
| const gridTopMargin = BUBBLE_RADIUS * 0.5; | |
| return row * (BUBBLE_RADIUS * 2 * 0.866) + BUBBLE_RADIUS + gridTopMargin; | |
| } | |
| function createGridBubble(col, row, color = null, powerUpType = POWERUP_TYPE.NONE) { | |
| return { | |
| color: color || BUBBLE_COLORS[Math.floor(Math.random() * BUBBLE_COLORS.length)], | |
| x: getBubbleX(col, row), | |
| y: getBubbleY(row), | |
| powerUpType: powerUpType, | |
| row: row, | |
| col: col | |
| }; | |
| } | |
| function createShooterBubble() { | |
| const existingColors = new Set(); | |
| let normalBubblesExist = false; | |
| for (let r = 0; r < ROWS; r++) { | |
| if (!bubbleGrid[r]) continue; | |
| for (let c = 0; c < getColsInRow(r); c++) { | |
| if (bubbleGrid[r][c] && bubbleGrid[r][c].powerUpType === POWERUP_TYPE.NONE) { | |
| existingColors.add(bubbleGrid[r][c].color); | |
| normalBubblesExist = true; | |
| } | |
| } | |
| } | |
| let availableColors = Array.from(existingColors); | |
| if (!normalBubblesExist || availableColors.length === 0) { // If only powerups left or grid is empty | |
| availableColors = [...BUBBLE_COLORS]; // Fallback to all colors | |
| } | |
| let powerUp = POWERUP_TYPE.NONE; | |
| let color = availableColors[Math.floor(Math.random() * availableColors.length)]; | |
| if (Math.random() < POWERUP_CHANCE) { | |
| const randPowerUpType = Math.random(); | |
| if (randPowerUpType < COLOR_SPLASH_CHANCE_OF_POWERUP) { | |
| powerUp = POWERUP_TYPE.COLOR_SPLASH; | |
| color = '#DDA0DD'; // Plum color for Color Splash | |
| } else if (randPowerUpType < COLOR_SPLASH_CHANCE_OF_POWERUP + 0.4) { // Bomb is common | |
| powerUp = POWERUP_TYPE.BOMB; | |
| color = '#333333'; | |
| } else { // Rainbow fills the rest | |
| powerUp = POWERUP_TYPE.RAINBOW; | |
| // Rainbow color is dynamic | |
| } | |
| } | |
| return { | |
| color: color, | |
| radius: BUBBLE_RADIUS, | |
| powerUpType: powerUp | |
| }; | |
| } | |
| function drawBubble(bubble) { | |
| const { x, y, color, radius, powerUpType } = bubble; | |
| if (!radius || radius <= 0) return; | |
| ctx.beginPath(); | |
| ctx.arc(x, y, radius, 0, Math.PI * 2); | |
| if (powerUpType === POWERUP_TYPE.RAINBOW) { | |
| const rainbowGradient = ctx.createRadialGradient(x, y, 0, x, y, radius); | |
| BUBBLE_COLORS.forEach((c, i) => rainbowGradient.addColorStop(Math.min(0.9, i / BUBBLE_COLORS.length), c)); // Cap stops for better blend | |
| rainbowGradient.addColorStop(1, BUBBLE_COLORS[0]); // Ensure full circle | |
| ctx.fillStyle = rainbowGradient; | |
| } else if (powerUpType === POWERUP_TYPE.COLOR_SPLASH) { | |
| const splashGradient = ctx.createRadialGradient(x,y,radius*0.2, x,y,radius); | |
| splashGradient.addColorStop(0, 'white'); | |
| splashGradient.addColorStop(0.3, color); // Its base color (e.g., Plum) | |
| splashGradient.addColorStop(0.6, BUBBLE_COLORS[Math.floor(Date.now()/500) % BUBBLE_COLORS.length]); // Cycling color | |
| splashGradient.addColorStop(1, color); | |
| ctx.fillStyle = splashGradient; | |
| } | |
| else { | |
| ctx.fillStyle = color; | |
| } | |
| ctx.fill(); | |
| // Shine effect | |
| 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); | |
| shineGradient.addColorStop(0, 'rgba(255, 255, 255, 0.6)'); | |
| shineGradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); | |
| ctx.fillStyle = shineGradient; | |
| ctx.fill(); | |
| // Icons for power-ups | |
| ctx.fillStyle = 'white'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| if (powerUpType === POWERUP_TYPE.BOMB) { | |
| ctx.font = `bold ${radius * 0.9}px Arial`; | |
| ctx.fillText('💣', x, y + radius*0.1); | |
| } else if (powerUpType === POWERUP_TYPE.COLOR_SPLASH) { | |
| ctx.font = `bold ${radius * 0.8}px Arial`; | |
| ctx.fillText('💦', x, y + radius*0.1); // Splash icon | |
| } | |
| ctx.strokeStyle = 'rgba(0,0,0,0.15)'; | |
| ctx.lineWidth = 1; | |
| ctx.stroke(); | |
| } | |
| function drawTrajectory() { | |
| if (!shooter.currentBubble || movingBubble) return; | |
| ctx.beginPath(); | |
| ctx.setLineDash([BUBBLE_RADIUS/3, BUBBLE_RADIUS/3]); | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; | |
| ctx.lineWidth = 2; | |
| let currentX = shooter.x; | |
| let currentY = shooter.y; | |
| let currentAngle = shooter.angle; | |
| let bounces = 0; | |
| const step = 5; // Small steps for trajectory line | |
| ctx.moveTo(currentX, currentY); | |
| for (let i = 0; i < 200; i++) { // Max trajectory length (number of steps) | |
| currentX += Math.cos(currentAngle) * step; | |
| currentY += Math.sin(currentAngle) * step; | |
| // Wall collision | |
| if (currentX <= BUBBLE_RADIUS || currentX >= canvas.width - BUBBLE_RADIUS) { | |
| if (bounces < MAX_TRAJECTORY_BOUNCES) { | |
| currentAngle = Math.PI - currentAngle; // Reflect angle | |
| // Adjust position slightly to prevent getting stuck in wall visually | |
| currentX = (currentX <= BUBBLE_RADIUS) ? BUBBLE_RADIUS + 1 : canvas.width - BUBBLE_RADIUS - 1; | |
| bounces++; | |
| ctx.lineTo(currentX, currentY); // Draw line to the bounce point | |
| ctx.stroke(); // Stroke the segment before bounce | |
| ctx.beginPath(); // Start new segment after bounce | |
| ctx.moveTo(currentX, currentY); | |
| } else { | |
| // Max bounces reached, stop drawing trajectory | |
| ctx.lineTo(currentX, currentY); | |
| break; | |
| } | |
| } | |
| // Top collision | |
| if (currentY <= BUBBLE_RADIUS) { | |
| currentY = BUBBLE_RADIUS; | |
| ctx.lineTo(currentX, currentY); | |
| break; | |
| } | |
| // Check for collision with existing grid bubbles (simplified for trajectory) | |
| let hitGridBubble = false; | |
| for (let r = 0; r < ROWS; r++) { | |
| if (!bubbleGrid[r]) continue; | |
| for (let c = 0; c < getColsInRow(r); c++) { | |
| if (bubbleGrid[r][c]) { | |
| const dist = Math.hypot(currentX - bubbleGrid[r][c].x, currentY - bubbleGrid[r][c].y); | |
| if (dist < BUBBLE_DIAMETER * 0.9) { // Collision with existing bubble | |
| hitGridBubble = true; | |
| break; | |
| } | |
| } | |
| } | |
| if (hitGridBubble) break; | |
| } | |
| if (hitGridBubble) { | |
| ctx.lineTo(currentX, currentY); | |
| break; | |
| } | |
| if (i % 2 === 0) { // Draw line in segments for dashed effect if setLineDash isn't perfect | |
| ctx.lineTo(currentX, currentY); | |
| } else { | |
| ctx.moveTo(currentX, currentY); | |
| } | |
| } | |
| ctx.stroke(); // Stroke the final segment | |
| // Draw a small circle at the predicted landing spot (end of trajectory) | |
| ctx.beginPath(); | |
| ctx.arc(currentX, currentY, BUBBLE_RADIUS * 0.3, 0, Math.PI * 2); | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; | |
| ctx.fill(); | |
| ctx.setLineDash([]); // Reset line dash | |
| } | |
| function drawGrid() { | |
| for (let r = 0; r < ROWS; r++) { | |
| if (!bubbleGrid[r]) continue; | |
| for (let c = 0; c < getColsInRow(r); c++) { | |
| if (bubbleGrid[r][c]) { | |
| drawBubble({ ...bubbleGrid[r][c], radius: BUBBLE_RADIUS, x: getBubbleX(c,r), y: getBubbleY(r) }); | |
| } | |
| } | |
| } | |
| } | |
| function drawShooter() { | |
| // Shooter Base | |
| ctx.fillStyle = '#444'; | |
| ctx.beginPath(); | |
| ctx.arc(shooter.x, shooter.y, BUBBLE_RADIUS * 1.2, Math.PI, 0); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.strokeStyle = '#222'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| // Draw enhanced trajectory line | |
| drawTrajectory(); | |
| if (shooter.currentBubble) { | |
| drawBubble({ ...shooter.currentBubble, x: shooter.x, y: shooter.y }); | |
| } | |
| if (shooter.nextBubble) { | |
| const nextBubbleX = shooter.nextBubbleArea.x + shooter.nextBubbleArea.width / 2; | |
| const nextBubbleY = shooter.nextBubbleArea.y + shooter.nextBubbleArea.height / 2; | |
| drawBubble({ ...shooter.nextBubble, x: nextBubbleX, y: nextBubbleY, radius: BUBBLE_RADIUS * 0.75 }); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = `${BUBBLE_RADIUS * 0.5}px Arial`; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('Next', nextBubbleX, nextBubbleY + BUBBLE_RADIUS * 0.75 + 8); | |
| } | |
| } | |
| function drawMovingBubble() { | |
| if (movingBubble) { | |
| drawBubble(movingBubble); | |
| } | |
| } | |
| function drawFallingBubbles() { | |
| for (let i = fallingBubbles.length - 1; i >= 0; i--) { | |
| const fb = fallingBubbles[i]; | |
| fb.y += fb.vy; | |
| fb.x += fb.vx; | |
| fb.vy += 0.2; | |
| fb.alpha -= 0.01; | |
| if (fb.alpha <= 0) { | |
| fallingBubbles.splice(i, 1); | |
| continue; | |
| } | |
| ctx.globalAlpha = fb.alpha; | |
| drawBubble({ ...fb, radius: BUBBLE_RADIUS }); | |
| ctx.globalAlpha = 1; | |
| } | |
| } | |
| function drawParticles() { | |
| for (let i = particles.length - 1; i >= 0; i--) { | |
| const p = particles[i]; | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| p.life--; | |
| p.alpha -= 0.02; | |
| if (p.life <= 0 || p.alpha <= 0) { | |
| particles.splice(i, 1); | |
| continue; | |
| } | |
| ctx.globalAlpha = p.alpha; | |
| ctx.fillStyle = p.color; | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.globalAlpha = 1; | |
| } | |
| } | |
| function createParticles(x, y, color, count = 10, speed = 4, sizeRange = [2,5]) { | |
| for (let i = 0; i < count; i++) { | |
| particles.push({ | |
| x, y, color, | |
| vx: (Math.random() - 0.5) * speed, | |
| vy: (Math.random() - 0.5) * speed, | |
| size: Math.random() * (sizeRange[1] - sizeRange[0]) + sizeRange[0], | |
| life: Math.random() * 30 + 30, // Duration in frames | |
| alpha: 1 | |
| }); | |
| } | |
| } | |
| function shoot() { | |
| // console.log("--- shoot() called ---"); | |
| if (currentGameState !== GAME_STATE.PLAYING || !shooter.currentBubble || movingBubble) { | |
| // console.log(`shoot() aborted: gameState=${currentGameState}, currentBubble=${!!shooter.currentBubble}, movingBubble=${!!movingBubble}`); | |
| return; | |
| } | |
| movingBubble = { | |
| ...shooter.currentBubble, | |
| x: shooter.x, | |
| y: shooter.y, | |
| vx: Math.cos(shooter.angle) * (BUBBLE_DIAMETER * 0.7), // Slightly faster speed | |
| vy: Math.sin(shooter.angle) * (BUBBLE_DIAMETER * 0.7) | |
| }; | |
| // console.log("Moving bubble created:", movingBubble); | |
| shooter.currentBubble = shooter.nextBubble; | |
| shooter.nextBubble = createShooterBubble(); | |
| shotsFiredSinceLastRow++; | |
| updateShotsDisplay(); | |
| isAiming = false; | |
| } | |
| function updateMovingBubble() { | |
| if (!movingBubble) return; | |
| movingBubble.x += movingBubble.vx; | |
| movingBubble.y += movingBubble.vy; | |
| if (movingBubble.x <= BUBBLE_RADIUS || movingBubble.x >= canvas.width - BUBBLE_RADIUS) { | |
| movingBubble.vx *= -1; | |
| movingBubble.x = Math.max(BUBBLE_RADIUS, Math.min(movingBubble.x, canvas.width - BUBBLE_RADIUS)); | |
| } | |
| if (movingBubble.y <= BUBBLE_RADIUS) { | |
| // console.log("Moving bubble hit top of screen."); | |
| movingBubble.y = BUBBLE_RADIUS; | |
| handleBubbleLanded(); | |
| return; | |
| } | |
| for (let r = 0; r < ROWS; r++) { | |
| if (!bubbleGrid[r]) continue; | |
| for (let c = 0; c < getColsInRow(r); c++) { | |
| if (bubbleGrid[r][c]) { | |
| const bubble = bubbleGrid[r][c]; | |
| const dist = Math.hypot(movingBubble.x - bubble.x, movingBubble.y - bubble.y); | |
| if (dist < BUBBLE_DIAMETER * 0.95) { | |
| // console.log(`Moving bubble collided with grid bubble at [${r}][${c}]`); | |
| handleBubbleLanded(); | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function handleBubbleLanded() { | |
| // console.log("--- handleBubbleLanded() ---"); | |
| if (currentGameState !== GAME_STATE.PLAYING) { | |
| if (movingBubble) movingBubble = null; | |
| return; | |
| } | |
| if(!movingBubble) return; | |
| const landedBubbleData = { ...movingBubble }; | |
| movingBubble = null; | |
| // console.log(`Landed bubble data: x=${landedBubbleData.x.toFixed(1)}, y=${landedBubbleData.y.toFixed(1)}, type=${landedBubbleData.powerUpType}, color=${landedBubbleData.color}`); | |
| // console.log(`Current GAME_OVER_ROW_INDEX for checks: ${GAME_OVER_ROW_INDEX}`); | |
| if (landedBubbleData.powerUpType === POWERUP_TYPE.BOMB) { | |
| explodeBomb(landedBubbleData.x, landedBubbleData.y); | |
| } else if (landedBubbleData.powerUpType === POWERUP_TYPE.COLOR_SPLASH) { | |
| activateColorSplash(landedBubbleData.x, landedBubbleData.y); | |
| } | |
| else { // Normal or Rainbow | |
| const { row, col } = findBestGridSlot(landedBubbleData.x, landedBubbleData.y); | |
| // console.log(`findBestGridSlot returned: target row=${row}, col=${col}.`); | |
| if (row !== -1 && col !== -1 && row < ROWS && col < getColsInRow(row) && bubbleGrid[row] && (bubbleGrid[row][col] === null || bubbleGrid[row][col] === undefined) ) { | |
| bubbleGrid[row][col] = createGridBubble(col, row, landedBubbleData.color, landedBubbleData.powerUpType); | |
| // console.log(`Placed bubble at [${row}][${col}]. Color: ${bubbleGrid[row][col].color}, Type: ${bubbleGrid[row][col].powerUpType}`); | |
| if (landedBubbleData.powerUpType === POWERUP_TYPE.RAINBOW) { | |
| activateRainbow(row, col); | |
| } else { | |
| checkMatches(row, col, landedBubbleData.color); | |
| } | |
| if (currentGameState === GAME_STATE.PLAYING && bubbleGrid[row] && bubbleGrid[row][col]) { | |
| // console.log(`Bubble at [${row}][${col}] still exists. Game Over Check: landed row (${row}) >= GAME_OVER_ROW_INDEX (${GAME_OVER_ROW_INDEX})`); | |
| if (row >= GAME_OVER_ROW_INDEX) { | |
| triggerGameOver(`Game Over: Bubble landed at row ${row}, which is >= game over line ${GAME_OVER_ROW_INDEX}.`); | |
| return; | |
| } | |
| } | |
| } else { | |
| // console.warn(`Could not place bubble. Slot [${row}][${col}] invalid or occupied. landedY=${landedBubbleData.y.toFixed(1)}`); | |
| const lowestGridBubbleY = getBubbleY(ROWS -1); | |
| if(landedBubbleData.y > lowestGridBubbleY + BUBBLE_RADIUS * 1.5) { | |
| triggerGameOver("Game Over: Bubble fell off bottom of grid!"); | |
| return; | |
| } | |
| } | |
| } | |
| if (currentGameState !== GAME_STATE.PLAYING) return; | |
| removeFloatingBubbles(); | |
| if (currentGameState !== GAME_STATE.PLAYING) return; | |
| let bubblesExist = false; | |
| for (let r = 0; r < ROWS; r++) { | |
| if (!bubbleGrid[r]) continue; | |
| for (let c = 0; c < getColsInRow(r); c++) { | |
| if (bubbleGrid[r][c]) {bubblesExist = true; break;} | |
| } | |
| if(bubblesExist) break; | |
| } | |
| if (!bubblesExist) { | |
| triggerYouWin(); | |
| return; | |
| } | |
| if (shotsFiredSinceLastRow >= SHOTS_UNTIL_NEW_ROW_THRESHOLD) { | |
| addNewRow(); | |
| shotsFiredSinceLastRow = 0; | |
| } else { | |
| checkIfGameOver(); | |
| } | |
| updateShotsDisplay(); | |
| // console.log("--- handleBubbleLanded() Finished ---"); | |
| } | |
| function findBestGridSlot(x, y) { | |
| let bestRow = -1, bestCol = -1; | |
| let minDist = Infinity; | |
| for (let r = 0; r < ROWS; r++) { | |
| if (!bubbleGrid[r]) continue; | |
| for (let c = 0; c < getColsInRow(r); c++) { | |
| const slotX = getBubbleX(c, r); | |
| const slotY = getBubbleY(r); | |
| const yBias = (ROWS - r) * 0.01 * BUBBLE_RADIUS; | |
| const dist = Math.hypot(x - slotX, y - slotY) - yBias; | |
| if (dist < minDist && dist < BUBBLE_DIAMETER * 1.6) { | |
| if (!bubbleGrid[r][c]) { | |
| minDist = dist; | |
| bestRow = r; | |
| bestCol = c; | |
| } | |
| } | |
| } | |
| } | |
| if (bestRow === -1) { | |
| // console.warn("findBestGridSlot: No ideal empty slot found, using more direct fallback."); | |
| let r_approx = 0; | |
| const firstRowY = getBubbleY(0); | |
| if (y < firstRowY - BUBBLE_RADIUS) { | |
| r_approx = 0; | |
| } else { | |
| r_approx = Math.floor((y - (firstRowY - BUBBLE_RADIUS)) / (BUBBLE_RADIUS * 2 * 0.866)); | |
| } | |
| r_approx = Math.max(0, Math.min(ROWS - 1, r_approx)); | |
| let c_approx = 0; | |
| if (bubbleGrid[r_approx]) { | |
| const offsetXForRow = (r_approx % 2 === 1) ? BUBBLE_RADIUS : 0; | |
| c_approx = Math.round((x - BUBBLE_RADIUS - offsetXForRow) / BUBBLE_DIAMETER); | |
| c_approx = Math.max(0, Math.min(getColsInRow(r_approx) - 1, c_approx)); | |
| } else { | |
| // console.error(`Fallback slot finding: Approximated Row ${r_approx} does not exist in bubbleGrid! THIS IS A BUG.`); | |
| return {row: -1, col: -1}; | |
| } | |
| if (bubbleGrid[r_approx] && !bubbleGrid[r_approx][c_approx]) { | |
| bestRow = r_approx; | |
| bestCol = c_approx; | |
| } else { | |
| if (bubbleGrid[r_approx]) { | |
| for (let offset = 1; offset <= Math.max(c_approx, getColsInRow(r_approx) - 1 - c_approx) + 1; offset++) { | |
| const c_left = c_approx - offset; | |
| const c_right = c_approx + offset; | |
| if (c_left >= 0 && c_left < getColsInRow(r_approx) && !bubbleGrid[r_approx][c_left]) { | |
| bestRow = r_approx; bestCol = c_left; break; | |
| } | |
| if (c_right < getColsInRow(r_approx) && c_right >=0 && !bubbleGrid[r_approx][c_right]) { | |
| bestRow = r_approx; bestCol = c_right; break; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return { row: bestRow, col: bestCol }; | |
| } | |
| function checkMatches(startRow, startCol, color) { | |
| if (currentGameState !== GAME_STATE.PLAYING) return false; | |
| if (!bubbleGrid[startRow] || !bubbleGrid[startRow][startCol] || bubbleGrid[startRow][startCol].powerUpType !== POWERUP_TYPE.NONE) { | |
| return false; | |
| } | |
| const toVisit = [{ r: startRow, c: startCol }]; | |
| const matched = []; | |
| const visited = new Set(); | |
| visited.add(`${startRow},${startCol}`); | |
| while (toVisit.length > 0) { | |
| const current = toVisit.pop(); | |
| if (bubbleGrid[current.r] && bubbleGrid[current.r][current.c] && | |
| bubbleGrid[current.r][current.c].color === color && | |
| bubbleGrid[current.r][current.c].powerUpType === POWERUP_TYPE.NONE) { | |
| matched.push(current); | |
| const neighbors = getNeighbors(current.r, current.c); | |
| for (const {r: nr, c: nc} of neighbors) { | |
| if (!visited.has(`${nr},${nc}`)) { | |
| visited.add(`${nr},${nc}`); | |
| toVisit.push({ r: nr, c: nc }); | |
| } | |
| } | |
| } | |
| } | |
| if (matched.length >= 3) { | |
| // console.log(`Found ${matched.length} matches starting from [${startRow}][${startCol}] of color ${color}`); | |
| matched.forEach(b => { | |
| if (bubbleGrid[b.r] && bubbleGrid[b.r][b.c]) { | |
| addFallingBubble(bubbleGrid[b.r][b.c]); | |
| createParticles(bubbleGrid[b.r][b.c].x, bubbleGrid[b.r][b.c].y, bubbleGrid[b.r][b.c].color, 15, 5, [2,4]); | |
| bubbleGrid[b.r][b.c] = null; | |
| score += 10; | |
| } | |
| }); | |
| updateScoreDisplay(); | |
| return true; | |
| } | |
| return false; | |
| } | |
| function activateRainbow(row, col) { | |
| // console.log(`--- activateRainbow at [${row}][${col}] ---`); | |
| if (currentGameState !== GAME_STATE.PLAYING) return; | |
| if (!bubbleGrid[row] || !bubbleGrid[row][col]) return; | |
| const rainbowBubbleOriginal = bubbleGrid[row][col]; | |
| addFallingBubble(rainbowBubbleOriginal); | |
| createParticles(rainbowBubbleOriginal.x, rainbowBubbleOriginal.y, 'white', 30, 6, [3,6]); | |
| bubbleGrid[row][col] = null; | |
| score += 20; // More points for rainbow | |
| // Determine target color: use next shooter bubble's color if it's normal | |
| let targetColor = null; | |
| if (shooter.nextBubble && shooter.nextBubble.powerUpType === POWERUP_TYPE.NONE) { | |
| targetColor = shooter.nextBubble.color; | |
| } else { // Fallback: find a common color on the board | |
| const colorCounts = {}; | |
| let maxCount = 0; | |
| for (let r_scan = 0; r_scan < ROWS; r_scan++) { | |
| if (!bubbleGrid[r_scan]) continue; | |
| for (let c_scan = 0; c_scan < getColsInRow(r_scan); c_scan++) { | |
| if (bubbleGrid[r_scan][c_scan] && bubbleGrid[r_scan][c_scan].powerUpType === POWERUP_TYPE.NONE) { | |
| const clr = bubbleGrid[r_scan][c_scan].color; | |
| colorCounts[clr] = (colorCounts[clr] || 0) + 1; | |
| if (colorCounts[clr] > maxCount) { | |
| maxCount = colorCounts[clr]; | |
| targetColor = clr; | |
| } | |
| } | |
| } | |
| } | |
| if (!targetColor) targetColor = BUBBLE_COLORS[0]; // Absolute fallback | |
| } | |
| // console.log("Rainbow target color:", targetColor); | |
| const neighbors = getNeighbors(row, col); | |
| let changedCount = 0; | |
| for (const {r: nr, c: nc} of neighbors) { | |
| if (bubbleGrid[nr] && bubbleGrid[nr][nc] && bubbleGrid[nr][nc].powerUpType === POWERUP_TYPE.NONE) { | |
| bubbleGrid[nr][nc].color = targetColor; | |
| createParticles(bubbleGrid[nr][nc].x, bubbleGrid[nr][nc].y, targetColor, 5, 2, [1,3]); | |
| changedCount++; | |
| } | |
| } | |
| // After changing colors, check for new matches around the affected area | |
| if (changedCount > 0) { | |
| // Check matches for each neighbor that was changed | |
| setTimeout(() => { // Slight delay to let color change render | |
| neighbors.forEach(({r: nr, c: nc}) => { | |
| if (bubbleGrid[nr] && bubbleGrid[nr][nc] && bubbleGrid[nr][nc].color === targetColor) { | |
| checkMatches(nr, nc, targetColor); | |
| } | |
| }); | |
| removeFloatingBubbles(); // Important after potential matches | |
| }, 50); | |
| } | |
| updateScoreDisplay(); | |
| // console.log("--- activateRainbow finished ---"); | |
| } | |
| function activateColorSplash(landedX, landedY) { | |
| // console.log(`--- activateColorSplash at x=${landedX.toFixed(1)}, y=${landedY.toFixed(1)} ---`); | |
| if (currentGameState !== GAME_STATE.PLAYING) return; | |
| createParticles(landedX, landedY, '#DDA0DD', 40, 7, [3,7]); // Plum colored particles | |
| score += 25; // Points for using Color Splash | |
| // Find all unique normal bubble colors on the board | |
| const currentBoardColors = new Set(); | |
| for (let r = 0; r < ROWS; r++) { | |
| if (!bubbleGrid[r]) continue; | |
| for (let c = 0; c < getColsInRow(r); c++) { | |
| if (bubbleGrid[r][c] && bubbleGrid[r][c].powerUpType === POWERUP_TYPE.NONE) { | |
| currentBoardColors.add(bubbleGrid[r][c].color); | |
| } | |
| } | |
| } | |
| const colorsArray = Array.from(currentBoardColors); | |
| if (colorsArray.length < 2) { // Need at least two colors to make a change meaningful | |
| // console.log("Color Splash: Not enough distinct colors on board to change."); | |
| updateScoreDisplay(); | |
| return; | |
| } | |
| const colorToRemove = colorsArray[Math.floor(Math.random() * colorsArray.length)]; | |
| let colorToBecome; | |
| do { | |
| colorToBecome = colorsArray[Math.floor(Math.random() * colorsArray.length)]; | |
| } while (colorToBecome === colorToRemove && colorsArray.length > 1); // Ensure it changes to a different color if possible | |
| // console.log(`Color Splash: Changing ${colorToRemove} to ${colorToBecome}`); | |
| let changedBubblesCoords = []; | |
| for (let r = 0; r < ROWS; r++) { | |
| if (!bubbleGrid[r]) continue; | |
| for (let c = 0; c < getColsInRow(r); c++) { | |
| if (bubbleGrid[r][c] && bubbleGrid[r][c].powerUpType === POWERUP_TYPE.NONE && bubbleGrid[r][c].color === colorToRemove) { | |
| bubbleGrid[r][c].color = colorToBecome; | |
| createParticles(bubbleGrid[r][c].x, bubbleGrid[r][c].y, colorToBecome, 3, 2, [1,2]); | |
| changedBubblesCoords.push({r,c}); | |
| } | |
| } | |
| } | |
| // After changing colors, check for new matches globally | |
| if (changedBubblesCoords.length > 0) { | |
| setTimeout(() => { // Slight delay | |
| let anyMatches = false; | |
| // Check matches starting from all changed bubbles | |
| changedBubblesCoords.forEach(coord => { | |
| if (bubbleGrid[coord.r] && bubbleGrid[coord.r][coord.c]) { // Check if bubble still exists | |
| if(checkMatches(coord.r, coord.c, colorToBecome)) anyMatches = true; | |
| } | |
| }); | |
| if (anyMatches) removeFloatingBubbles(); | |
| }, 50); | |
| } | |
| updateScoreDisplay(); | |
| // console.log("--- activateColorSplash finished ---"); | |
| } | |
| function explodeBomb(x, y) { | |
| // console.log(`--- explodeBomb at x=${x.toFixed(1)}, y=${y.toFixed(1)} ---`); | |
| if (currentGameState !== GAME_STATE.PLAYING) return; | |
| createParticles(x, y, '#FFA500', 60, 8, [3,8]); // More particles for bomb | |
| score += 15; | |
| let clearedCount = 0; | |
| for (let r = 0; r < ROWS; r++) { | |
| if (!bubbleGrid[r]) continue; | |
| for (let c = 0; c < getColsInRow(r); c++) { | |
| if (bubbleGrid[r][c]) { | |
| const dist = Math.hypot(x - bubbleGrid[r][c].x, y - bubbleGrid[r][c].y); | |
| if (dist < BUBBLE_RADIUS * BOMB_RADIUS_MULTIPLIER) { | |
| // console.log(`Bomb clearing bubble at [${r}][${c}]`); | |
| addFallingBubble(bubbleGrid[r][c]); | |
| // Different particle color for bubbles popped by bomb | |
| createParticles(bubbleGrid[r][c].x, bubbleGrid[r][c].y, bubbleGrid[r][c].color || '#888888', 8, 4, [2,4]); | |
| bubbleGrid[r][c] = null; | |
| score += 5; // Extra per bubble in bomb | |
| clearedCount++; | |
| } | |
| } | |
| } | |
| } | |
| if (clearedCount > 0) updateScoreDisplay(); | |
| // console.log(`Bomb cleared ${clearedCount} bubbles.`); | |
| } | |
| function getNeighbors(r, c) { | |
| const neighbors = []; | |
| const isOddRow = r % 2 === 1; | |
| const deltas = [ | |
| { dr: -1, dc: isOddRow ? 0 : -1 }, { dr: -1, dc: isOddRow ? 1 : 0 }, | |
| { dr: 0, dc: -1 }, { dr: 0, dc: 1 }, | |
| { dr: 1, dc: isOddRow ? 0 : -1 }, { dr: 1, dc: isOddRow ? 1 : 0 } | |
| ]; | |
| deltas.forEach(d => { | |
| const nr = r + d.dr; | |
| const nc = c + d.dc; | |
| if (nr >= 0 && nr < ROWS && nc >= 0 && nc < getColsInRow(nr)) { | |
| neighbors.push({ r: nr, c: nc }); | |
| } | |
| }); | |
| return neighbors; | |
| } | |
| function addFallingBubble(gridBubble) { | |
| if (!gridBubble) return; | |
| fallingBubbles.push({ | |
| ...gridBubble, | |
| x: gridBubble.x, | |
| y: gridBubble.y, | |
| vx: (Math.random() - 0.5) * 2.5, // Slightly more horizontal spread | |
| vy: -Math.random() * 2.5 - 1.5, // Stronger initial pop | |
| alpha: 1, | |
| radius: BUBBLE_RADIUS | |
| }); | |
| } | |
| function removeFloatingBubbles() { | |
| // console.log("--- removeFloatingBubbles ---"); | |
| if (currentGameState !== GAME_STATE.PLAYING) return; | |
| const connected = new Set(); | |
| for (let c = 0; c < getColsInRow(0); c++) { | |
| if (bubbleGrid[0] && bubbleGrid[0][c]) { | |
| markConnected(0, c, connected); | |
| } | |
| } | |
| let floatingCleared = 0; | |
| for (let r = 0; r < ROWS; r++) { | |
| if (!bubbleGrid[r]) continue; | |
| for (let c = 0; c < getColsInRow(r); c++) { | |
| if (bubbleGrid[r][c] && !connected.has(`${r},${c}`)) { | |
| addFallingBubble(bubbleGrid[r][c]); | |
| createParticles(bubbleGrid[r][c].x, bubbleGrid[r][c].y, bubbleGrid[r][c].color, 5,3,[1,3]); // Small pop for floating | |
| bubbleGrid[r][c] = null; | |
| score += 5; | |
| floatingCleared++; | |
| } | |
| } | |
| } | |
| if (floatingCleared > 0) { | |
| // console.log(`Cleared ${floatingCleared} floating bubbles.`); | |
| updateScoreDisplay(); | |
| } | |
| } | |
| function markConnected(r, c, connectedSet) { | |
| const key = `${r},${c}`; | |
| if (connectedSet.has(key) || !bubbleGrid[r] || !bubbleGrid[r][c]) return; | |
| connectedSet.add(key); | |
| const neighbors = getNeighbors(r, c); | |
| neighbors.forEach(n => markConnected(n.r, n.c, connectedSet)); | |
| } | |
| function addNewRow() { | |
| // console.log("--- addNewRow ---"); | |
| if (currentGameState !== GAME_STATE.PLAYING) return; | |
| const checkRowForPush = GAME_OVER_ROW_INDEX - 1; | |
| // console.log(`addNewRow: Checking for game over push at row ${checkRowForPush} (GAME_OVER_ROW_INDEX is ${GAME_OVER_ROW_INDEX})`); | |
| if (checkRowForPush >= 0) { | |
| if (bubbleGrid[checkRowForPush]) { | |
| for (let c = 0; c < getColsInRow(checkRowForPush); c++) { | |
| if (bubbleGrid[checkRowForPush][c]) { | |
| // console.log(`addNewRow: Bubble found at [${checkRowForPush}][${c}], will cause game over on shift.`); | |
| shiftRowsDown(); | |
| triggerGameOver(`Game Over: New row pushed bubbles to game over line ${GAME_OVER_ROW_INDEX}.`); | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| // console.log("addNewRow: No immediate game over from push. Shifting rows down."); | |
| shiftRowsDown(); | |
| bubbleGrid[0] = new Array(getColsInRow(0)).fill(null); | |
| for (let c = 0; c < getColsInRow(0); c++) { | |
| if (Math.random() < 0.65) { | |
| bubbleGrid[0][c] = createGridBubble(c, 0); | |
| } | |
| } | |
| // console.log("New top row created."); | |
| checkIfGameOver(); | |
| } | |
| function shiftRowsDown() { | |
| // console.log("Shifting rows down..."); | |
| for (let r = ROWS - 1; r > 0; r--) { | |
| bubbleGrid[r] = bubbleGrid[r-1]; | |
| if (bubbleGrid[r]) { | |
| for(let c = 0; c < getColsInRow(r); c++) { | |
| if (bubbleGrid[r][c]) { | |
| bubbleGrid[r][c].row = r; | |
| bubbleGrid[r][c].y = getBubbleY(r); | |
| bubbleGrid[r][c].x = getBubbleX(c,r); | |
| } | |
| } | |
| } | |
| } | |
| bubbleGrid[0] = new Array(getColsInRow(0)).fill(null); | |
| } | |
| function checkIfGameOver() { | |
| // console.log(`--- checkIfGameOver (using GAME_OVER_ROW_INDEX: ${GAME_OVER_ROW_INDEX}) ---`); | |
| if (currentGameState !== GAME_STATE.PLAYING) return; | |
| if (GAME_OVER_ROW_INDEX < 0 || GAME_OVER_ROW_INDEX >= ROWS) return; | |
| if (bubbleGrid[GAME_OVER_ROW_INDEX]) { | |
| for (let c = 0; c < getColsInRow(GAME_OVER_ROW_INDEX); c++) { | |
| if (bubbleGrid[GAME_OVER_ROW_INDEX][c]) { | |
| triggerGameOver(`Game Over: Bubble found at [${GAME_OVER_ROW_INDEX}][${c}] on game over line.`); | |
| return; | |
| } | |
| } | |
| } | |
| // console.log("checkIfGameOver: No bubbles on game over line."); | |
| } | |
| function updateScoreDisplay() { scoreElement.textContent = score; } | |
| function updateShotsDisplay() { shotsUntilNextRowElement.textContent = Math.max(0, SHOTS_UNTIL_NEW_ROW_THRESHOLD - shotsFiredSinceLastRow); } | |
| function triggerGameOver(message = "Game Over!") { | |
| if (currentGameState === GAME_STATE.GAME_OVER || currentGameState === GAME_STATE.YOU_WIN) return; | |
| currentGameState = GAME_STATE.GAME_OVER; | |
| overlayTitle.textContent = message; | |
| overlayScore.textContent = `Final Score: ${score}`; | |
| overlayScreen.style.display = 'flex'; | |
| console.error("GAME OVER TRIGGERED:", message, `Score: ${score}`); | |
| } | |
| function triggerYouWin() { | |
| if (currentGameState === GAME_STATE.YOU_WIN || currentGameState === GAME_STATE.GAME_OVER) return; | |
| currentGameState = GAME_STATE.YOU_WIN; | |
| overlayTitle.textContent = "Congratulations! You Win!"; | |
| overlayScore.textContent = `Final Score: ${score}`; | |
| overlayScreen.style.display = 'flex'; | |
| // console.log("%c YOU WIN! ", "background: green; color: white; font-size: 20px;", `Score: ${score}`); | |
| } | |
| function handleMouseDown(e) { | |
| if (currentGameState !== GAME_STATE.PLAYING || movingBubble) return; | |
| e.preventDefault(); | |
| const pos = getMousePos(e); | |
| const area = shooter.nextBubbleArea; | |
| if (pos.x >= area.x && pos.x <= area.x + area.width && | |
| pos.y >= area.y && pos.y <= area.y + area.height) { | |
| if (shooter.currentBubble && shooter.nextBubble) { | |
| [shooter.currentBubble, shooter.nextBubble] = [shooter.nextBubble, shooter.currentBubble]; | |
| // console.log("Bubbles swapped."); | |
| } | |
| return; | |
| } | |
| isAiming = true; | |
| updateAimAngle(pos.x, pos.y); | |
| } | |
| function handleMouseMove(e) { | |
| if (currentGameState !== GAME_STATE.PLAYING || !isAiming || movingBubble) return; | |
| e.preventDefault(); | |
| const pos = getMousePos(e); | |
| updateAimAngle(pos.x, pos.y); | |
| } | |
| function handleMouseUp(e) { | |
| if (currentGameState !== GAME_STATE.PLAYING || !isAiming || movingBubble) { | |
| isAiming = false; | |
| return; | |
| } | |
| e.preventDefault(); | |
| shoot(); | |
| isAiming = false; | |
| } | |
| function getMousePos(evt) { | |
| const rect = canvas.getBoundingClientRect(); | |
| let clientX, clientY; | |
| if (evt.touches && evt.touches.length > 0) { | |
| clientX = evt.touches[0].clientX; | |
| clientY = evt.touches[0].clientY; | |
| } else { | |
| clientX = evt.clientX; | |
| clientY = evt.clientY; | |
| } | |
| return { | |
| x: clientX - rect.left, | |
| y: clientY - rect.top | |
| }; | |
| } | |
| function updateAimAngle(mouseX, mouseY) { | |
| let dx = mouseX - shooter.x; | |
| let dy = mouseY - shooter.y; | |
| shooter.angle = Math.atan2(dy, dx); | |
| const minAngle = -Math.PI * 0.96; // Slightly wider allowed angle | |
| const maxAngle = -Math.PI * 0.04; | |
| shooter.angle = Math.max(minAngle, Math.min(shooter.angle, maxAngle)); | |
| } | |
| function gameLoop() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| if (currentGameState === GAME_STATE.MENU) { | |
| ctx.fillStyle = '#000022'; | |
| ctx.fillRect(0,0,canvas.width, canvas.height); | |
| } else if (currentGameState === GAME_STATE.PLAYING) { | |
| updateMovingBubble(); | |
| drawGrid(); | |
| drawShooter(); // This now calls drawTrajectory | |
| drawMovingBubble(); | |
| drawFallingBubbles(); | |
| drawParticles(); | |
| } else if (currentGameState === GAME_STATE.GAME_OVER || currentGameState === GAME_STATE.YOU_WIN) { | |
| drawGrid(); | |
| drawShooter(); | |
| drawFallingBubbles(); | |
| drawParticles(); | |
| } | |
| requestAnimationFrame(gameLoop); | |
| } | |
| startButton.addEventListener('click', () => { | |
| // console.log("Start button clicked."); | |
| setupCanvas(); | |
| initGame(); | |
| }); | |
| restartButton.addEventListener('click', () => { | |
| // console.log("Restart button clicked."); | |
| setupCanvas(); | |
| initGame(); | |
| }); | |
| menuButton.addEventListener('click', () => { | |
| // console.log("Menu button clicked."); | |
| currentGameState = GAME_STATE.MENU; | |
| overlayScreen.style.display = 'none'; | |
| startScreen.style.display = 'flex'; | |
| }); | |
| canvas.addEventListener('mousedown', handleMouseDown); | |
| canvas.addEventListener('mousemove', handleMouseMove); | |
| canvas.addEventListener('mouseup', handleMouseUp); | |
| canvas.addEventListener('touchstart', handleMouseDown, { passive: false }); | |
| canvas.addEventListener('touchmove', handleMouseMove, { passive: false }); | |
| canvas.addEventListener('touchend', handleMouseUp, { passive: false }); | |
| // console.log("Initial page load: Setting up canvas for MENU state display and starting game loop."); | |
| setupCanvas(); | |
| gameLoop(); | |
| </script> | |
| </body> | |
| </html> |