bublleshoote3 / index.html
offerpk3's picture
Update index.html
a4c6e33 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enhanced Bubble Shooter Game</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;
}
.game-container {
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
text-align: center; /* Center canvas and UI elements */
}
canvas {
border: 3px solid #fff;
border-radius: 10px;
background: #000033; /* Darker blue for contrast */
display: block;
margin: 0 auto; /* Center canvas */
}
.ui {
color: white;
margin-bottom: 15px;
display: flex;
justify-content: space-around;
align-items: center;
}
.score, .shots-info {
font-size: 20px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
.message-area {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.7);
color: white;
padding: 20px 40px;
border-radius: 10px;
font-size: 28px;
font-weight: bold;
display: none; /* Hidden by default */
z-index: 100;
text-align: center;
}
.controls {
margin-top: 15px;
color: rgba(255, 255, 255, 0.85);
font-size: 14px;
}
button {
background-color: #ff6b6b;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
transition: background-color 0.3s ease;
margin-top: 10px;
}
button:hover {
background-color: #ff4757;
}
#restartButton {
display: none; /* Hidden by default */
}
</style>
</head>
<body>
<div class="game-container">
<div class="ui">
<div class="score">Score: <span id="score">0</span></div>
<div class="shots-info">Next Row In: <span id="shotsUntilNextRow">0</span> shots</div>
</div>
<canvas id="gameCanvas" width="600" height="700"></canvas>
<div class="controls">
Mouse se aim karo aur click karke bubble shoot karo!<br>
Same color ke 3+ bubbles ko connect karo
</div>
<button id="restartButton">Restart Game</button>
</div>
<div id="messageDisplay" class="message-area"></div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreElement = document.getElementById('score');
const shotsUntilNextRowElement = document.getElementById('shotsUntilNextRow');
const messageDisplayElement = document.getElementById('messageDisplay');
const restartButton = document.getElementById('restartButton');
// Game constants
const BUBBLE_RADIUS = 18; // Slightly smaller for more bubbles
const BUBBLE_COLORS = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3', '#54a0ff', '#ff7f50']; // Added one more color
const ROWS = 16; // Grid dimensions
const COLS = Math.floor(canvas.width / (BUBBLE_RADIUS * 2)); // Dynamic cols based on canvas width and radius
const INIT_ROWS_COUNT = 6; // Initial rows of bubbles
const SHOTS_UNTIL_NEW_ROW_THRESHOLD = 7; // Shots before new row
const GAME_OVER_ROW_INDEX = ROWS - 1; // If a bubble reaches this row index
// Game variables
let score = 0;
let gameRunning = true;
let shotsFiredSinceLastRow = 0;
const bubbleGrid = [];
const shooter = {
x: canvas.width / 2,
y: canvas.height - 40, // Moved shooter slightly up
angle: -Math.PI / 2, // Default aim upwards
currentBubble: null,
nextBubble: null
};
let movingBubble = null;
function init() {
score = 0;
shotsFiredSinceLastRow = 0;
gameRunning = true;
movingBubble = null;
messageDisplayElement.style.display = 'none';
restartButton.style.display = 'none';
bubbleGrid.length = 0; // Clear existing grid
for (let row = 0; row < ROWS; row++) {
bubbleGrid[row] = new Array(COLS).fill(null);
}
// Create initial bubble grid
for (let row = 0; row < INIT_ROWS_COUNT; row++) {
for (let col = 0; col < (row % 2 === 1 ? COLS -1 : COLS) ; col++) {
if (Math.random() < 0.75) { // Density of initial bubbles
bubbleGrid[row][col] = {
color: BUBBLE_COLORS[Math.floor(Math.random() * BUBBLE_COLORS.length)],
x: getBubbleX(col, row),
y: getBubbleY(row)
};
}
}
}
shooter.currentBubble = createRandomBubble();
shooter.nextBubble = createRandomBubble();
updateScoreDisplay();
updateShotsDisplay();
}
function getBubbleX(col, row) {
const offsetX = (row % 2 === 1) ? BUBBLE_RADIUS : 0;
return col * (BUBBLE_RADIUS * 2) + BUBBLE_RADIUS + offsetX;
}
function getBubbleY(row) {
// Use a slight overlap for tighter packing (sqrt(3) * R for perfect hex)
return row * (BUBBLE_RADIUS * 2 * 0.866) + BUBBLE_RADIUS + 10; // +10 to push grid down a bit
}
function createRandomBubble() {
// Try to provide colors that are present on the board
const existingColors = new Set();
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (bubbleGrid[r][c]) {
existingColors.add(bubbleGrid[r][c].color);
}
}
}
let availableColors = Array.from(existingColors);
if (availableColors.length === 0) { // If grid is empty or has no colors somehow
availableColors = [...BUBBLE_COLORS];
}
return {
color: availableColors[Math.floor(Math.random() * availableColors.length)],
radius: BUBBLE_RADIUS
};
}
function drawBubble(x, y, color, radius = BUBBLE_RADIUS) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
const gradient = ctx.createRadialGradient(x - radius*0.3, y - radius*0.3, 0, x, y, radius);
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.5)');
gradient.addColorStop(0.8, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradient;
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.2)'; // Darker stroke for definition
ctx.lineWidth = 1;
ctx.stroke();
}
function drawGrid() {
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < (row % 2 === 1 ? COLS -1 : COLS) ; col++) {
if (bubbleGrid[row][col]) {
const bubble = bubbleGrid[row][col];
drawBubble(bubble.x, bubble.y, bubble.color);
}
}
}
}
function drawShooter() {
// Draw shooter base (more styled)
ctx.fillStyle = '#555';
ctx.beginPath();
ctx.moveTo(shooter.x - 25, shooter.y + 15);
ctx.lineTo(shooter.x + 25, shooter.y + 15);
ctx.lineTo(shooter.x, shooter.y - 15);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.stroke();
// Draw trajectory line
drawTrajectory();
if (shooter.currentBubble) {
drawBubble(shooter.x, shooter.y, shooter.currentBubble.color);
}
if (shooter.nextBubble) {
drawBubble(shooter.x + 55, shooter.y + 15, shooter.nextBubble.color, BUBBLE_RADIUS * 0.75);
ctx.fillStyle = 'white';
ctx.font = '11px Arial';
ctx.textAlign = 'center';
ctx.fillText('Next', shooter.x + 55, shooter.y + 15 + BUBBLE_RADIUS *0.75 + 12);
}
}
function drawTrajectory() {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]); // Dashed line
let x = shooter.x;
let y = shooter.y;
let angle = shooter.angle;
ctx.beginPath();
ctx.moveTo(x, y);
for (let i = 0; i < 150; i++) { // Max length of trajectory line
x += Math.cos(angle) * 5; // Small steps for trajectory
y += Math.sin(angle) * 5;
if (x <= BUBBLE_RADIUS || x >= canvas.width - BUBBLE_RADIUS) {
// Reflect angle for wall bounce
angle = Math.PI - angle;
// Ensure it doesn't get stuck by slightly adjusting position
x = (x <= BUBBLE_RADIUS) ? BUBBLE_RADIUS + 1 : canvas.width - BUBBLE_RADIUS - 1;
}
if (y <= BUBBLE_RADIUS) { // Hit top
y = BUBBLE_RADIUS;
ctx.lineTo(x,y);
break;
}
// Simplified: doesn't check trajectory collision with existing bubbles
ctx.lineTo(x,y);
}
ctx.stroke();
ctx.setLineDash([]); // Reset line dash
}
function drawMovingBubble() {
if (movingBubble) {
drawBubble(movingBubble.x, movingBubble.y, movingBubble.color);
}
}
function shoot() {
if (!gameRunning || !shooter.currentBubble || movingBubble) return;
movingBubble = {
x: shooter.x,
y: shooter.y,
color: shooter.currentBubble.color,
vx: Math.cos(shooter.angle) * 10, // Increased speed
vy: Math.sin(shooter.angle) * 10,
radius: BUBBLE_RADIUS
};
shooter.currentBubble = shooter.nextBubble;
shooter.nextBubble = createRandomBubble();
shotsFiredSinceLastRow++;
updateShotsDisplay();
}
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 = -movingBubble.vx;
// Clamp position to prevent sticking in wall
movingBubble.x = Math.max(BUBBLE_RADIUS, Math.min(movingBubble.x, canvas.width - BUBBLE_RADIUS));
}
if (movingBubble.y <= BUBBLE_RADIUS) {
movingBubble.y = BUBBLE_RADIUS; // Snap to top if it goes above
attachBubble();
return;
}
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < (row % 2 === 1 ? COLS -1 : COLS); col++) {
if (bubbleGrid[row][col]) {
const bubble = bubbleGrid[row][col];
const distance = Math.sqrt(
(movingBubble.x - bubble.x) ** 2 +
(movingBubble.y - bubble.y) ** 2
);
if (distance < BUBBLE_RADIUS * 1.9) { // Collision threshold (slightly less than 2R)
attachBubble();
return;
}
}
}
}
}
function attachBubble() {
if (!movingBubble || !gameRunning) return;
let bestRow = -1, bestCol = -1;
let minDistance = Infinity;
// Iterate over potential grid slots
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < (r % 2 === 1 ? COLS -1 : COLS); c++) {
if (bubbleGrid[r][c]) continue; // Slot already taken
const gridX = getBubbleX(c, r);
const gridY = getBubbleY(r);
const distance = Math.sqrt((movingBubble.x - gridX) ** 2 + (movingBubble.y - gridY) ** 2);
// Prioritize slots closer to the bubble's impact point and higher up
if (distance < BUBBLE_RADIUS * 2.5 && distance < minDistance) {
minDistance = distance;
bestRow = r;
bestCol = c;
}
}
}
// If no suitable empty slot found nearby (e.g. flying into dense cluster),
// try to find the closest grid point based on y, then x.
if (bestRow === -1 || bestCol === -1) {
// Fallback: simple row/col calculation based on y/x
let targetRow = Math.round((movingBubble.y - BUBBLE_RADIUS -10) / (BUBBLE_RADIUS * 2 * 0.866));
targetRow = Math.max(0, Math.min(ROWS - 1, targetRow));
let targetCol = Math.round((movingBubble.x - BUBBLE_RADIUS - (targetRow % 2 === 1 ? BUBBLE_RADIUS : 0)) / (BUBBLE_RADIUS * 2));
targetCol = Math.max(0, Math.min((targetRow % 2 === 1 ? COLS -2 : COLS -1) , targetCol));
if (!bubbleGrid[targetRow][targetCol]) {
bestRow = targetRow;
bestCol = targetCol;
} else { // If still can't find, place it at the earliest possible slot
// This part needs more robust logic for "forcing" a position if all else fails
// For now, if movingBubble.y is very low, it might be an issue.
// Let's assume it finds a spot for now or game over will trigger.
console.warn("Could not find ideal slot, bubble might be lost or placed oddly.");
// Attempt to place in the row determined by movingBubble.y, first available column
for (let c_fallback = 0; c_fallback < (targetRow % 2 === 1 ? COLS -1 : COLS); c_fallback++) {
if (!bubbleGrid[targetRow][c_fallback]) {
bestRow = targetRow; bestCol = c_fallback; break;
}
}
}
}
if (bestRow !== -1 && bestCol !== -1) {
bubbleGrid[bestRow][bestCol] = {
color: movingBubble.color,
x: getBubbleX(bestCol, bestRow),
y: getBubbleY(bestRow)
};
movingBubble = null; // Bubble is now part of the grid
const matches = checkMatches(bestRow, bestCol);
if (bestRow >= GAME_OVER_ROW_INDEX && bubbleGrid[bestRow][bestCol]) {
gameOver("Bubbles reached the bottom!");
return;
}
if (matches.length === 0 && shotsFiredSinceLastRow >= SHOTS_UNTIL_NEW_ROW_THRESHOLD) {
addNewRow();
shotsFiredSinceLastRow = 0;
updateShotsDisplay();
}
checkWinCondition();
} else { // Should not happen often with fallback, but if it does:
console.error("Failed to attach bubble!");
movingBubble = null; // Prevent infinite loop
checkGameOver(); // Check if this state is game over
}
}
function checkMatches(row, col) {
if (!bubbleGrid[row] || !bubbleGrid[row][col]) return [];
const targetColor = bubbleGrid[row][col].color;
const toProcess = [{r: row, c: col}];
const matchedBubbles = [];
const visited = new Set();
visited.add(`${row},${col}`);
while(toProcess.length > 0) {
const current = toProcess.pop();
matchedBubbles.push(current);
const neighbors = getNeighbors(current.r, current.c);
for (const [nr, nc] of neighbors) {
const key = `${nr},${nc}`;
if (!visited.has(key) && bubbleGrid[nr] && bubbleGrid[nr][nc] && bubbleGrid[nr][nc].color === targetColor) {
visited.add(key);
toProcess.push({r: nr, c: nc});
}
}
}
if (matchedBubbles.length >= 3) {
matchedBubbles.forEach(match => {
bubbleGrid[match.r][match.c] = null;
});
score += matchedBubbles.length * 10;
updateScoreDisplay();
removeFloatingBubbles();
return matchedBubbles; // Return actual matches
}
return []; // No matches of 3 or more
}
function getNeighbors(row, col) {
const neighbors = [];
const isOddRow = row % 2 === 1;
const deltas = [
// Top row neighbors
{ dr: -1, dc: isOddRow ? 0 : -1 }, { dr: -1, dc: isOddRow ? 1 : 0 },
// Same row neighbors
{ dr: 0, dc: -1 }, { dr: 0, dc: 1 },
// Bottom row neighbors
{ dr: 1, dc: isOddRow ? 0 : -1 }, { dr: 1, dc: isOddRow ? 1 : 0 }
];
deltas.forEach(delta => {
const nr = row + delta.dr;
const nc = col + delta.dc;
if (nr >= 0 && nr < ROWS && nc >= 0 && nc < (nr % 2 === 1 ? COLS -1: COLS)) {
// Check specific COLS limit for odd/even rows
if (bubbleGrid[nr] !== undefined) { // Ensure row exists
neighbors.push([nr, nc]);
}
}
});
return neighbors;
}
function removeFloatingBubbles() {
const connectedToTop = new Set();
for (let c = 0; c < COLS; c++) {
if (bubbleGrid[0][c]) {
markConnected(0, c, connectedToTop);
}
}
let floatingCount = 0;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < (r % 2 === 1 ? COLS -1: COLS); c++) {
if (bubbleGrid[r][c] && !connectedToTop.has(`${r},${c}`)) {
bubbleGrid[r][c] = null;
floatingCount++;
}
}
}
if (floatingCount > 0) {
score += floatingCount * 15; // Bonus for floating
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(([nr, nc]) => markConnected(nr, nc, connectedSet));
}
function addNewRow() {
if (!gameRunning) return;
// First, check if adding a new row would immediately cause game over
// by pushing existing bubbles into the game over zone.
for (let c = 0; c < ( (GAME_OVER_ROW_INDEX -1) % 2 === 1 ? COLS -1 : COLS ); c++) {
if (bubbleGrid[GAME_OVER_ROW_INDEX - 1] && bubbleGrid[GAME_OVER_ROW_INDEX - 1][c]) {
gameOver("Bubbles overflowed!");
// Shift for visual effect then show game over
// Perform the shift anyway so player sees it happen
shiftRowsDown();
bubbleGrid[GAME_OVER_ROW_INDEX][c] = bubbleGrid[GAME_OVER_ROW_INDEX-1][c];
if( bubbleGrid[GAME_OVER_ROW_INDEX][c]) {
bubbleGrid[GAME_OVER_ROW_INDEX][c].y = getBubbleY(GAME_OVER_ROW_INDEX);
bubbleGrid[GAME_OVER_ROW_INDEX][c].x = getBubbleX(c, GAME_OVER_ROW_INDEX);
}
return;
}
}
shiftRowsDown();
// Create new top row
bubbleGrid[0] = new Array(COLS).fill(null);
for (let c = 0; c < COLS; c++) {
// For row 0 (even), max columns is COLS
if (Math.random() < 0.6) { // Density of new row
bubbleGrid[0][c] = {
color: BUBBLE_COLORS[Math.floor(Math.random() * BUBBLE_COLORS.length)],
x: getBubbleX(c, 0),
y: getBubbleY(0)
};
}
}
checkGameOver(); // Check after new row fully added
}
function shiftRowsDown() {
for (let r = ROWS - 1; r > 0; r--) {
bubbleGrid[r] = bubbleGrid[r - 1];
if (bubbleGrid[r]) {
for (let c = 0; c < (r % 2 === 1 ? COLS -1 : COLS); c++) {
if (bubbleGrid[r][c]) {
bubbleGrid[r][c].y = getBubbleY(r);
bubbleGrid[r][c].x = getBubbleX(c,r); // Important to update x too due to row offset
}
}
}
}
}
function checkGameOver() {
if (!gameRunning) return;
for (let c = 0; c < (GAME_OVER_ROW_INDEX % 2 === 1 ? COLS -1 : COLS); c++) {
if (bubbleGrid[GAME_OVER_ROW_INDEX] && bubbleGrid[GAME_OVER_ROW_INDEX][c]) {
gameOver("Bubbles reached the bottom line!");
return;
}
}
}
function checkWinCondition() {
if (!gameRunning) return;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < (r % 2 === 1 ? COLS -1 : COLS); c++) {
if (bubbleGrid[r][c]) return; // Found a bubble, game not won
}
}
gameWon("You cleared all bubbles! YOU WIN!");
}
function gameOver(message = "Game Over!") {
gameRunning = false;
messageDisplayElement.textContent = message;
messageDisplayElement.style.display = 'block';
restartButton.style.display = 'block';
console.log("Game Over:", message);
}
function gameWon(message = "You Win!") {
gameRunning = false;
messageDisplayElement.textContent = message;
messageDisplayElement.style.display = 'block';
restartButton.style.display = 'block';
console.log("Game Won:", message);
}
function updateScoreDisplay() {
scoreElement.textContent = score;
}
function updateShotsDisplay() {
const shotsLeft = SHOTS_UNTIL_NEW_ROW_THRESHOLD - shotsFiredSinceLastRow;
shotsUntilNextRowElement.textContent = Math.max(0, shotsLeft);
}
// Event handlers
canvas.addEventListener('mousemove', (e) => {
if (!gameRunning) return;
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
shooter.angle = Math.atan2(mouseY - shooter.y, mouseX - shooter.x);
// Limit angle to upward shots (approx -5 to -175 degrees)
const minAngle = -Math.PI * 0.95; // About -171 degrees
const maxAngle = -Math.PI * 0.05; // About -9 degrees
shooter.angle = Math.max(minAngle, Math.min(shooter.angle, maxAngle));
});
canvas.addEventListener('click', () => {
if (gameRunning) {
shoot();
}
});
restartButton.addEventListener('click', () => {
init();
gameLoop(); // Restart the game loop
});
function gameLoop() {
if (!gameRunning) {
// Draw one last time to show final state (e.g., game over bubbles)
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid();
drawShooter(); // Still draw shooter for context
// No moving bubble when game is over
return; // Stop the loop
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
updateMovingBubble();
drawGrid();
drawShooter();
drawMovingBubble();
requestAnimationFrame(gameLoop);
}
// Start game
init();
gameLoop();
</script>
</body>
</html>