User prompt
move it again
User prompt
move it again
User prompt
move ‘centreCircle’ down by 50 units
User prompt
AGAIN
User prompt
again
User prompt
move the preview bubble down by 20 units
User prompt
move it again but up by 50 units
User prompt
again
User prompt
again but by 20 units
User prompt
move the image up by 10 units
User prompt
make the image bigger by 50 units again
User prompt
Make the image that is behind the next bubble 50 units bigger
User prompt
Make the button 50 units bigger
User prompt
Create an image that goes behind the next bubble
User prompt
Make it so depending on the bonus, the pop sound increases in volume, for example, 1.1x bonus = 110% volume, 1.2x bonus = 120% volume
User prompt
Create an image for the line coming from the cannon
User prompt
Create an image for true line coming out of the cannon ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make it so when you buy a rainbow bubble, you have it as your next bubble and then you can shoot a bubble
User prompt
create a button to buy a rainbow bubble for 500 points
User prompt
If 5 bubbles are popped at once, make the points given be 1.1x more and the set it to 0, if 6, then 1.2x and so on
User prompt
When a bubble hits either walls, the bubble stays there as if it had hit another bubble, getting stuck there until 2 other bubbles of the same colour hit the bubble and pop it
User prompt
Make the rainbow bubble also pop the bubbles of the same colour of the bubble it touched that it is connected to
User prompt
When the rainbow bubble pops a bubble, make it pop itself aswell
User prompt
Again
User prompt
Again
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Bubble class var Bubble = Container.expand(function () { var self = Container.call(this); // Properties self.color = null; // 'red', 'green', etc. self.gridRow = null; self.gridCol = null; self.isMoving = false; // True if this bubble is flying (shot) self.radius = 60; // Half of asset width/height // Attach asset self.setColor = function (color) { self.color = color; if (self.bubbleAsset) { self.removeChild(self.bubbleAsset); } var assetId = color === 'rainbow' ? 'bubble_rainbow' : 'bubble_' + color; self.bubbleAsset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); }; // For flying bubbles, set velocity self.vx = 0; self.vy = 0; // For popping animation self.pop = function (_onFinish) { tween(self, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { if (_onFinish) _onFinish(); } }); }; // Update for moving bubbles self.update = function () { if (self.isMoving) { self.x += self.vx; self.y += self.vy; } }; return self; }); // Cannon class var Cannon = Container.expand(function () { var self = Container.call(this); // Attach cannon asset self.cannonAsset = self.attachAsset('cannon', { anchorX: 0.5, anchorY: 0.5 }); // Angle in radians (0 = up) self.angle = 0; // Set angle and rotate cannon self.setAngle = function (angle) { self.angle = angle; self.cannonAsset.rotation = angle; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222244 }); /**** * Game Code ****/ // New wall image asset // Rainbow bubble: white color, acts as all colors // Bubble shoot sound // Cannon // Bubble colors // --- Game constants --- var GAME_W = 2048; var GAME_H = 2732; var BUBBLE_RADIUS = 60; // px var BUBBLE_DIAM = 120; var GRID_COLS = 12; // Number of columns var GRID_ROWS = 14; // Number of rows var GRID_TOP = 200; // px from top var GRID_LEFT = (GAME_W - GRID_COLS * BUBBLE_DIAM) / 2; // Center grid var COLORS = ['red', 'green', 'blue', 'yellow', 'purple']; var SHOOTABLE_COLORS = ['red', 'green', 'blue', 'yellow', 'purple', 'rainbow']; var SHOOT_SPEED = 38; // px per frame var MIN_ANGLE = -Math.PI / 2 + Math.PI / 8; // -67.5 deg var MAX_ANGLE = -Math.PI / 2 - Math.PI / 8; // -112.5 deg // --- Game state --- var grid = []; // 2D array [row][col] of Bubble or null var flyingBubble = null; // The bubble currently being shot var nextBubbleColor = null; // Color of next bubble var cannon = null; var score = 0; var scoreTxt = null; var isShooting = false; var gameOver = false; // --- GUI --- scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // --- Helper functions --- // Get pixel position for grid cell function gridToXY(row, col) { var x = GRID_LEFT + col * BUBBLE_DIAM + BUBBLE_RADIUS; var y = GRID_TOP + row * BUBBLE_DIAM + BUBBLE_RADIUS; // Odd rows are offset (hex grid) if (row % 2 === 1) x += BUBBLE_RADIUS; return { x: x, y: y }; } // Get grid cell for pixel position function xyToGrid(x, y) { // Estimate row var row = Math.round((y - GRID_TOP - BUBBLE_RADIUS) / BUBBLE_DIAM); if (row < 0) row = 0; if (row >= GRID_ROWS) row = GRID_ROWS - 1; // Estimate col var col = Math.round((x - GRID_LEFT - BUBBLE_RADIUS - (row % 2 === 1 ? BUBBLE_RADIUS : 0)) / BUBBLE_DIAM); if (col < 0) col = 0; if (col >= GRID_COLS) col = GRID_COLS - 1; return { row: row, col: col }; } // Check if a grid cell is valid function isValidCell(row, col) { if (row < 0 || row >= GRID_ROWS) return false; if (col < 0 || col >= GRID_COLS) return false; return true; } // Get neighbors (hex grid) function getNeighbors(row, col) { // Even/odd row offset var even = row % 2 === 0; var neighbors = [{ row: row - 1, col: col }, // up { row: row - 1, col: col + (even ? -1 : 1) }, // up-left/up-right { row: row, col: col - 1 }, // left { row: row, col: col + 1 }, // right { row: row + 1, col: col }, // down { row: row + 1, col: col + (even ? -1 : 1) } // down-left/down-right ]; // Filter valid var valid = []; for (var i = 0; i < neighbors.length; ++i) { var n = neighbors[i]; if (isValidCell(n.row, n.col)) valid.push(n); } return valid; } // Find all connected bubbles of the same color (DFS) function findConnected(row, col, color, visited) { if (!isValidCell(row, col)) return; if (visited[row * GRID_COLS + col]) return; var b = grid[row][col]; // Rainbow bubble matches any color, and any color matches rainbow if (!b) return; if (color === 'rainbow' || b.color === 'rainbow') { // Allow connection } else if (b.color !== color) { return; } visited[row * GRID_COLS + col] = true; var neighbors = getNeighbors(row, col); for (var i = 0; i < neighbors.length; ++i) { var n = neighbors[i]; findConnected(n.row, n.col, color, visited); } } // Find all bubbles connected to the top (DFS) function findConnectedToTop(visited) { for (var col = 0; col < GRID_COLS; ++col) { dfsTop(0, col, visited); } } function dfsTop(row, col, visited) { if (!isValidCell(row, col)) return; if (visited[row * GRID_COLS + col]) return; var b = grid[row][col]; if (!b) return; visited[row * GRID_COLS + col] = true; var neighbors = getNeighbors(row, col); for (var i = 0; i < neighbors.length; ++i) { var n = neighbors[i]; dfsTop(n.row, n.col, visited); } } // Remove bubbles in visited function removeBubbles(visited, onPop) { var popped = 0; for (var row = 0; row < GRID_ROWS; ++row) { for (var col = 0; col < GRID_COLS; ++col) { if (visited[row * GRID_COLS + col]) { var b = grid[row][col]; if (b) { (function (bubble, row, col) { bubble.pop(function () { if (bubble.parent) bubble.parent.removeChild(bubble); }); })(b, row, col); grid[row][col] = null; popped++; } } } } if (popped > 0 && onPop) LK.getSound('bubble_pop').play(); return popped; } // Check for game over (bubbles at bottom row) function checkGameOver() { for (var col = 0; col < GRID_COLS; ++col) { if (grid[GRID_ROWS - 1][col]) { return true; } } return false; } // Check for win (all bubbles cleared) function checkWin() { for (var row = 0; row < GRID_ROWS; ++row) { for (var col = 0; col < GRID_COLS; ++col) { if (grid[row][col]) return false; } } return true; } // Get a random color from colors present on the board function getRandomColorOnBoard() { var present = {}; for (var row = 0; row < GRID_ROWS; ++row) { for (var col = 0; col < GRID_COLS; ++col) { var b = grid[row][col]; if (b) present[b.color] = true; } } var arr = []; for (var i = 0; i < COLORS.length; ++i) { if (present[COLORS[i]]) arr.push(COLORS[i]); } if (arr.length === 0) arr = COLORS.slice(); return arr[Math.floor(Math.random() * arr.length)]; } // --- Game setup --- // Initialize grid function initGrid() { grid = []; for (var row = 0; row < GRID_ROWS; ++row) { var arr = []; for (var col = 0; col < GRID_COLS; ++col) { arr.push(null); } grid.push(arr); } // Fill first 9 rows with random bubbles for (var row = 0; row < 9; ++row) { for (var col = 0; col < GRID_COLS; ++col) { // Odd rows have one less bubble at the end if (row % 2 === 1 && col === GRID_COLS - 1) continue; var color = COLORS[Math.floor(Math.random() * COLORS.length)]; var bubble = new Bubble(); bubble.setColor(color); var pos = gridToXY(row, col); bubble.x = pos.x; bubble.y = pos.y; bubble.gridRow = row; bubble.gridCol = col; grid[row][col] = bubble; game.addChild(bubble); } } } // Initialize cannon function initCannon() { cannon = new Cannon(); cannon.x = GAME_W / 2; cannon.y = GAME_H - 180; cannon.setAngle(-Math.PI / 2); // Up game.addChild(cannon); } // Prepare next bubble color function prepareNextBubble() { // Only allow rainbow as a shootable bubble, and make it rare (1 in 30 chance) if (Math.floor(Math.random() * 30) === 0) { nextBubbleColor = 'rainbow'; } else { // Pick from non-rainbow colors present on the board, or all if none var present = {}; for (var row = 0; row < GRID_ROWS; ++row) { for (var col = 0; col < GRID_COLS; ++col) { var b = grid[row][col]; if (b && b.color !== 'rainbow') present[b.color] = true; } } var arr = []; for (var i = 0; i < COLORS.length; ++i) { if (present[COLORS[i]]) arr.push(COLORS[i]); } if (arr.length === 0) arr = COLORS.slice(); nextBubbleColor = arr[Math.floor(Math.random() * arr.length)]; } // Always update preview bubble if it exists if (previewBubble) { previewBubble.setColor(nextBubbleColor); } } // Launch a new flying bubble function launchBubble(angle) { if (isShooting || gameOver) return; isShooting = true; var bubble = new Bubble(); bubble.setColor(nextBubbleColor); bubble.x = cannon.x; bubble.y = cannon.y - 80; bubble.isMoving = true; // Set velocity bubble.vx = Math.cos(angle) * SHOOT_SPEED; bubble.vy = Math.sin(angle) * SHOOT_SPEED; flyingBubble = bubble; game.addChild(bubble); LK.getSound('bubble_shoot').play(); prepareNextBubble(); } // Snap flying bubble to grid function snapBubbleToGrid(bubble) { // Find nearest empty cell var minDist = 99999; var bestRow = 0, bestCol = 0; for (var row = 0; row < GRID_ROWS; ++row) { for (var col = 0; col < GRID_COLS; ++col) { // Odd rows have one less bubble at the end if (row % 2 === 1 && col === GRID_COLS - 1) continue; if (grid[row][col]) continue; var pos = gridToXY(row, col); var dx = bubble.x - pos.x; var dy = bubble.y - pos.y; var dist = dx * dx + dy * dy; if (dist < minDist) { minDist = dist; bestRow = row; bestCol = col; } } } // Place bubble var pos = gridToXY(bestRow, bestCol); bubble.x = pos.x; bubble.y = pos.y; bubble.isMoving = false; bubble.vx = 0; bubble.vy = 0; bubble.gridRow = bestRow; bubble.gridCol = bestCol; grid[bestRow][bestCol] = bubble; flyingBubble = null; isShooting = false; // Check for matches var visited = {}; if (bubble.color === 'rainbow') { // Rainbow only pops the colored bubble it lands on (if any neighbor) var neighbors = getNeighbors(bestRow, bestCol); var found = false; for (var i = 0; i < neighbors.length; ++i) { var n = neighbors[i]; var nb = grid[n.row][n.col]; if (nb && nb.color !== 'rainbow') { visited[n.row * GRID_COLS + n.col] = true; found = true; break; // Only pop one colored neighbor } } } else { findConnected(bestRow, bestCol, bubble.color, visited); } // Count connected var count = 0; for (var k in visited) if (visited[k]) count++; if (count >= 3 || bubble.color === 'rainbow' && count > 0) { // Remove connected or single colored neighbor for rainbow var popped = removeBubbles(visited, true); score += popped * 10; scoreTxt.setText(score); // Remove unattached bubbles var attached = {}; findConnectedToTop(attached); var floating = {}; for (var row = 0; row < GRID_ROWS; ++row) { for (var col = 0; col < GRID_COLS; ++col) { if (grid[row][col] && !attached[row * GRID_COLS + col]) { floating[row * GRID_COLS + col] = true; } } } var dropped = removeBubbles(floating, true); if (dropped > 0) { score += dropped * 20; scoreTxt.setText(score); } } // Check for win/lose if (bubble.color !== 'rainbow' && checkWin()) { LK.showYouWin(); gameOver = true; return; } if (checkGameOver()) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); gameOver = true; return; } } // --- Game setup --- initGrid(); initCannon(); prepareNextBubble(); // --- Add left and right wall boxes --- var wallThickness = 280; var wallHeight = GAME_H; var leftWall = LK.getAsset('wall_image', { anchorX: 0, anchorY: 0, width: wallThickness, height: wallHeight, x: 0, y: 0 }); var rightWall = LK.getAsset('wall_image', { anchorX: 0, anchorY: 0, width: wallThickness, height: wallHeight, x: GAME_W - wallThickness, y: 0 }); game.addChild(leftWall); game.addChild(rightWall); // --- Draw next bubble preview --- // Create preview bubble immediately so it always shows var previewBubble = new Bubble(); previewBubble.x = GAME_W - 320; // Moved 200 units to the left previewBubble.y = GAME_H - 120; // Set the initial preview bubble color if (nextBubbleColor) { previewBubble.setColor(nextBubbleColor); } else { // If nextBubbleColor isn't set yet, use a default color previewBubble.setColor(COLORS[Math.floor(Math.random() * COLORS.length)]); } game.addChild(previewBubble); // --- Input handling --- // Drag to aim var aiming = false; var aimAngle = -Math.PI / 2; // Up // For drawing the aim line var aimLine = null; var aimLineStart = { x: 0, y: 0 }; var aimLineEnd = { x: 0, y: 0 }; // Helper to create or update the aim line function updateAimLine(startX, startY, endX, endY) { // Remove old aimLine if exists if (aimLine && aimLine.parent) { aimLine.parent.removeChild(aimLine); } // Create a new Container for the line aimLine = new Container(); // Draw the line using a series of small bubbles (dots) between start and end var dx = endX - startX; var dy = endY - startY; var dist = Math.sqrt(dx * dx + dy * dy); var steps = Math.floor(dist / 32); for (var i = 0; i <= steps; ++i) { var t = i / steps; var px = startX + dx * t; var py = startY + dy * t; // Use a small, semi-transparent white bubble as a dot var dot = LK.getAsset('bubble_blue', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.18, scaleY: 0.18, alpha: 0.35 }); dot.x = px; dot.y = py; aimLine.addChild(dot); } // Add to game game.addChild(aimLine); } function clamp(val, min, max) { if (val < min) return min; if (val > max) return max; return val; } game.down = function (x, y, obj) { if (gameOver) return; aiming = true; handleAim(x, y); }; game.move = function (x, y, obj) { if (aiming && !isShooting && !gameOver) { handleAim(x, y); } }; game.up = function (x, y, obj) { if (aiming && !isShooting && !gameOver) { launchBubble(aimAngle); } aiming = false; // Remove aim line when shot if (aimLine && aimLine.parent) { aimLine.parent.removeChild(aimLine); aimLine = null; } }; function handleAim(x, y) { // Calculate angle from cannon to (x, y) var dx = x - cannon.x; var dy = y - cannon.y; var angle = Math.atan2(dy, dx); // Clamp angle to allowed range angle = clamp(angle, MAX_ANGLE, MIN_ANGLE); aimAngle = angle; cannon.setAngle(angle); // Draw aim line from cannon tip to the aiming point var startX = cannon.x + Math.cos(angle) * 100; var startY = cannon.y + Math.sin(angle) * 100; var endX = cannon.x + Math.cos(angle) * 1200; var endY = cannon.y + Math.sin(angle) * 1200; updateAimLine(startX, startY, endX, endY); } // --- Game update loop --- game.update = function () { // Update flying bubble if (flyingBubble && flyingBubble.isMoving) { flyingBubble.update(); // Bounce off walls if (flyingBubble.x < BUBBLE_RADIUS) { flyingBubble.x = BUBBLE_RADIUS; flyingBubble.vx = -flyingBubble.vx; } if (flyingBubble.x > GAME_W - BUBBLE_RADIUS) { flyingBubble.x = GAME_W - BUBBLE_RADIUS; flyingBubble.vx = -flyingBubble.vx; } // Check collision with top if (flyingBubble.y < GRID_TOP + BUBBLE_RADIUS) { snapBubbleToGrid(flyingBubble); } else { // Check collision with grid bubbles var hit = false; for (var row = 0; row < GRID_ROWS; ++row) { for (var col = 0; col < GRID_COLS; ++col) { var b = grid[row][col]; if (!b) continue; var dx = flyingBubble.x - b.x; var dy = flyingBubble.y - b.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < BUBBLE_DIAM - 2) { snapBubbleToGrid(flyingBubble); hit = true; break; } } if (hit) break; } // If bubble falls below bottom, game over if (flyingBubble && flyingBubble.y > GAME_H - 100) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); gameOver = true; return; } } } // Update preview bubble color if (previewBubble && nextBubbleColor && previewBubble.color !== nextBubbleColor) { previewBubble.setColor(nextBubbleColor); } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Bubble class
var Bubble = Container.expand(function () {
var self = Container.call(this);
// Properties
self.color = null; // 'red', 'green', etc.
self.gridRow = null;
self.gridCol = null;
self.isMoving = false; // True if this bubble is flying (shot)
self.radius = 60; // Half of asset width/height
// Attach asset
self.setColor = function (color) {
self.color = color;
if (self.bubbleAsset) {
self.removeChild(self.bubbleAsset);
}
var assetId = color === 'rainbow' ? 'bubble_rainbow' : 'bubble_' + color;
self.bubbleAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
};
// For flying bubbles, set velocity
self.vx = 0;
self.vy = 0;
// For popping animation
self.pop = function (_onFinish) {
tween(self, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
if (_onFinish) _onFinish();
}
});
};
// Update for moving bubbles
self.update = function () {
if (self.isMoving) {
self.x += self.vx;
self.y += self.vy;
}
};
return self;
});
// Cannon class
var Cannon = Container.expand(function () {
var self = Container.call(this);
// Attach cannon asset
self.cannonAsset = self.attachAsset('cannon', {
anchorX: 0.5,
anchorY: 0.5
});
// Angle in radians (0 = up)
self.angle = 0;
// Set angle and rotate cannon
self.setAngle = function (angle) {
self.angle = angle;
self.cannonAsset.rotation = angle;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222244
});
/****
* Game Code
****/
// New wall image asset
// Rainbow bubble: white color, acts as all colors
// Bubble shoot sound
// Cannon
// Bubble colors
// --- Game constants ---
var GAME_W = 2048;
var GAME_H = 2732;
var BUBBLE_RADIUS = 60; // px
var BUBBLE_DIAM = 120;
var GRID_COLS = 12; // Number of columns
var GRID_ROWS = 14; // Number of rows
var GRID_TOP = 200; // px from top
var GRID_LEFT = (GAME_W - GRID_COLS * BUBBLE_DIAM) / 2; // Center grid
var COLORS = ['red', 'green', 'blue', 'yellow', 'purple'];
var SHOOTABLE_COLORS = ['red', 'green', 'blue', 'yellow', 'purple', 'rainbow'];
var SHOOT_SPEED = 38; // px per frame
var MIN_ANGLE = -Math.PI / 2 + Math.PI / 8; // -67.5 deg
var MAX_ANGLE = -Math.PI / 2 - Math.PI / 8; // -112.5 deg
// --- Game state ---
var grid = []; // 2D array [row][col] of Bubble or null
var flyingBubble = null; // The bubble currently being shot
var nextBubbleColor = null; // Color of next bubble
var cannon = null;
var score = 0;
var scoreTxt = null;
var isShooting = false;
var gameOver = false;
// --- GUI ---
scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Helper functions ---
// Get pixel position for grid cell
function gridToXY(row, col) {
var x = GRID_LEFT + col * BUBBLE_DIAM + BUBBLE_RADIUS;
var y = GRID_TOP + row * BUBBLE_DIAM + BUBBLE_RADIUS;
// Odd rows are offset (hex grid)
if (row % 2 === 1) x += BUBBLE_RADIUS;
return {
x: x,
y: y
};
}
// Get grid cell for pixel position
function xyToGrid(x, y) {
// Estimate row
var row = Math.round((y - GRID_TOP - BUBBLE_RADIUS) / BUBBLE_DIAM);
if (row < 0) row = 0;
if (row >= GRID_ROWS) row = GRID_ROWS - 1;
// Estimate col
var col = Math.round((x - GRID_LEFT - BUBBLE_RADIUS - (row % 2 === 1 ? BUBBLE_RADIUS : 0)) / BUBBLE_DIAM);
if (col < 0) col = 0;
if (col >= GRID_COLS) col = GRID_COLS - 1;
return {
row: row,
col: col
};
}
// Check if a grid cell is valid
function isValidCell(row, col) {
if (row < 0 || row >= GRID_ROWS) return false;
if (col < 0 || col >= GRID_COLS) return false;
return true;
}
// Get neighbors (hex grid)
function getNeighbors(row, col) {
// Even/odd row offset
var even = row % 2 === 0;
var neighbors = [{
row: row - 1,
col: col
},
// up
{
row: row - 1,
col: col + (even ? -1 : 1)
},
// up-left/up-right
{
row: row,
col: col - 1
},
// left
{
row: row,
col: col + 1
},
// right
{
row: row + 1,
col: col
},
// down
{
row: row + 1,
col: col + (even ? -1 : 1)
} // down-left/down-right
];
// Filter valid
var valid = [];
for (var i = 0; i < neighbors.length; ++i) {
var n = neighbors[i];
if (isValidCell(n.row, n.col)) valid.push(n);
}
return valid;
}
// Find all connected bubbles of the same color (DFS)
function findConnected(row, col, color, visited) {
if (!isValidCell(row, col)) return;
if (visited[row * GRID_COLS + col]) return;
var b = grid[row][col];
// Rainbow bubble matches any color, and any color matches rainbow
if (!b) return;
if (color === 'rainbow' || b.color === 'rainbow') {
// Allow connection
} else if (b.color !== color) {
return;
}
visited[row * GRID_COLS + col] = true;
var neighbors = getNeighbors(row, col);
for (var i = 0; i < neighbors.length; ++i) {
var n = neighbors[i];
findConnected(n.row, n.col, color, visited);
}
}
// Find all bubbles connected to the top (DFS)
function findConnectedToTop(visited) {
for (var col = 0; col < GRID_COLS; ++col) {
dfsTop(0, col, visited);
}
}
function dfsTop(row, col, visited) {
if (!isValidCell(row, col)) return;
if (visited[row * GRID_COLS + col]) return;
var b = grid[row][col];
if (!b) return;
visited[row * GRID_COLS + col] = true;
var neighbors = getNeighbors(row, col);
for (var i = 0; i < neighbors.length; ++i) {
var n = neighbors[i];
dfsTop(n.row, n.col, visited);
}
}
// Remove bubbles in visited
function removeBubbles(visited, onPop) {
var popped = 0;
for (var row = 0; row < GRID_ROWS; ++row) {
for (var col = 0; col < GRID_COLS; ++col) {
if (visited[row * GRID_COLS + col]) {
var b = grid[row][col];
if (b) {
(function (bubble, row, col) {
bubble.pop(function () {
if (bubble.parent) bubble.parent.removeChild(bubble);
});
})(b, row, col);
grid[row][col] = null;
popped++;
}
}
}
}
if (popped > 0 && onPop) LK.getSound('bubble_pop').play();
return popped;
}
// Check for game over (bubbles at bottom row)
function checkGameOver() {
for (var col = 0; col < GRID_COLS; ++col) {
if (grid[GRID_ROWS - 1][col]) {
return true;
}
}
return false;
}
// Check for win (all bubbles cleared)
function checkWin() {
for (var row = 0; row < GRID_ROWS; ++row) {
for (var col = 0; col < GRID_COLS; ++col) {
if (grid[row][col]) return false;
}
}
return true;
}
// Get a random color from colors present on the board
function getRandomColorOnBoard() {
var present = {};
for (var row = 0; row < GRID_ROWS; ++row) {
for (var col = 0; col < GRID_COLS; ++col) {
var b = grid[row][col];
if (b) present[b.color] = true;
}
}
var arr = [];
for (var i = 0; i < COLORS.length; ++i) {
if (present[COLORS[i]]) arr.push(COLORS[i]);
}
if (arr.length === 0) arr = COLORS.slice();
return arr[Math.floor(Math.random() * arr.length)];
}
// --- Game setup ---
// Initialize grid
function initGrid() {
grid = [];
for (var row = 0; row < GRID_ROWS; ++row) {
var arr = [];
for (var col = 0; col < GRID_COLS; ++col) {
arr.push(null);
}
grid.push(arr);
}
// Fill first 9 rows with random bubbles
for (var row = 0; row < 9; ++row) {
for (var col = 0; col < GRID_COLS; ++col) {
// Odd rows have one less bubble at the end
if (row % 2 === 1 && col === GRID_COLS - 1) continue;
var color = COLORS[Math.floor(Math.random() * COLORS.length)];
var bubble = new Bubble();
bubble.setColor(color);
var pos = gridToXY(row, col);
bubble.x = pos.x;
bubble.y = pos.y;
bubble.gridRow = row;
bubble.gridCol = col;
grid[row][col] = bubble;
game.addChild(bubble);
}
}
}
// Initialize cannon
function initCannon() {
cannon = new Cannon();
cannon.x = GAME_W / 2;
cannon.y = GAME_H - 180;
cannon.setAngle(-Math.PI / 2); // Up
game.addChild(cannon);
}
// Prepare next bubble color
function prepareNextBubble() {
// Only allow rainbow as a shootable bubble, and make it rare (1 in 30 chance)
if (Math.floor(Math.random() * 30) === 0) {
nextBubbleColor = 'rainbow';
} else {
// Pick from non-rainbow colors present on the board, or all if none
var present = {};
for (var row = 0; row < GRID_ROWS; ++row) {
for (var col = 0; col < GRID_COLS; ++col) {
var b = grid[row][col];
if (b && b.color !== 'rainbow') present[b.color] = true;
}
}
var arr = [];
for (var i = 0; i < COLORS.length; ++i) {
if (present[COLORS[i]]) arr.push(COLORS[i]);
}
if (arr.length === 0) arr = COLORS.slice();
nextBubbleColor = arr[Math.floor(Math.random() * arr.length)];
}
// Always update preview bubble if it exists
if (previewBubble) {
previewBubble.setColor(nextBubbleColor);
}
}
// Launch a new flying bubble
function launchBubble(angle) {
if (isShooting || gameOver) return;
isShooting = true;
var bubble = new Bubble();
bubble.setColor(nextBubbleColor);
bubble.x = cannon.x;
bubble.y = cannon.y - 80;
bubble.isMoving = true;
// Set velocity
bubble.vx = Math.cos(angle) * SHOOT_SPEED;
bubble.vy = Math.sin(angle) * SHOOT_SPEED;
flyingBubble = bubble;
game.addChild(bubble);
LK.getSound('bubble_shoot').play();
prepareNextBubble();
}
// Snap flying bubble to grid
function snapBubbleToGrid(bubble) {
// Find nearest empty cell
var minDist = 99999;
var bestRow = 0,
bestCol = 0;
for (var row = 0; row < GRID_ROWS; ++row) {
for (var col = 0; col < GRID_COLS; ++col) {
// Odd rows have one less bubble at the end
if (row % 2 === 1 && col === GRID_COLS - 1) continue;
if (grid[row][col]) continue;
var pos = gridToXY(row, col);
var dx = bubble.x - pos.x;
var dy = bubble.y - pos.y;
var dist = dx * dx + dy * dy;
if (dist < minDist) {
minDist = dist;
bestRow = row;
bestCol = col;
}
}
}
// Place bubble
var pos = gridToXY(bestRow, bestCol);
bubble.x = pos.x;
bubble.y = pos.y;
bubble.isMoving = false;
bubble.vx = 0;
bubble.vy = 0;
bubble.gridRow = bestRow;
bubble.gridCol = bestCol;
grid[bestRow][bestCol] = bubble;
flyingBubble = null;
isShooting = false;
// Check for matches
var visited = {};
if (bubble.color === 'rainbow') {
// Rainbow only pops the colored bubble it lands on (if any neighbor)
var neighbors = getNeighbors(bestRow, bestCol);
var found = false;
for (var i = 0; i < neighbors.length; ++i) {
var n = neighbors[i];
var nb = grid[n.row][n.col];
if (nb && nb.color !== 'rainbow') {
visited[n.row * GRID_COLS + n.col] = true;
found = true;
break; // Only pop one colored neighbor
}
}
} else {
findConnected(bestRow, bestCol, bubble.color, visited);
}
// Count connected
var count = 0;
for (var k in visited) if (visited[k]) count++;
if (count >= 3 || bubble.color === 'rainbow' && count > 0) {
// Remove connected or single colored neighbor for rainbow
var popped = removeBubbles(visited, true);
score += popped * 10;
scoreTxt.setText(score);
// Remove unattached bubbles
var attached = {};
findConnectedToTop(attached);
var floating = {};
for (var row = 0; row < GRID_ROWS; ++row) {
for (var col = 0; col < GRID_COLS; ++col) {
if (grid[row][col] && !attached[row * GRID_COLS + col]) {
floating[row * GRID_COLS + col] = true;
}
}
}
var dropped = removeBubbles(floating, true);
if (dropped > 0) {
score += dropped * 20;
scoreTxt.setText(score);
}
}
// Check for win/lose
if (bubble.color !== 'rainbow' && checkWin()) {
LK.showYouWin();
gameOver = true;
return;
}
if (checkGameOver()) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
gameOver = true;
return;
}
}
// --- Game setup ---
initGrid();
initCannon();
prepareNextBubble();
// --- Add left and right wall boxes ---
var wallThickness = 280;
var wallHeight = GAME_H;
var leftWall = LK.getAsset('wall_image', {
anchorX: 0,
anchorY: 0,
width: wallThickness,
height: wallHeight,
x: 0,
y: 0
});
var rightWall = LK.getAsset('wall_image', {
anchorX: 0,
anchorY: 0,
width: wallThickness,
height: wallHeight,
x: GAME_W - wallThickness,
y: 0
});
game.addChild(leftWall);
game.addChild(rightWall);
// --- Draw next bubble preview ---
// Create preview bubble immediately so it always shows
var previewBubble = new Bubble();
previewBubble.x = GAME_W - 320; // Moved 200 units to the left
previewBubble.y = GAME_H - 120;
// Set the initial preview bubble color
if (nextBubbleColor) {
previewBubble.setColor(nextBubbleColor);
} else {
// If nextBubbleColor isn't set yet, use a default color
previewBubble.setColor(COLORS[Math.floor(Math.random() * COLORS.length)]);
}
game.addChild(previewBubble);
// --- Input handling ---
// Drag to aim
var aiming = false;
var aimAngle = -Math.PI / 2; // Up
// For drawing the aim line
var aimLine = null;
var aimLineStart = {
x: 0,
y: 0
};
var aimLineEnd = {
x: 0,
y: 0
};
// Helper to create or update the aim line
function updateAimLine(startX, startY, endX, endY) {
// Remove old aimLine if exists
if (aimLine && aimLine.parent) {
aimLine.parent.removeChild(aimLine);
}
// Create a new Container for the line
aimLine = new Container();
// Draw the line using a series of small bubbles (dots) between start and end
var dx = endX - startX;
var dy = endY - startY;
var dist = Math.sqrt(dx * dx + dy * dy);
var steps = Math.floor(dist / 32);
for (var i = 0; i <= steps; ++i) {
var t = i / steps;
var px = startX + dx * t;
var py = startY + dy * t;
// Use a small, semi-transparent white bubble as a dot
var dot = LK.getAsset('bubble_blue', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.18,
scaleY: 0.18,
alpha: 0.35
});
dot.x = px;
dot.y = py;
aimLine.addChild(dot);
}
// Add to game
game.addChild(aimLine);
}
function clamp(val, min, max) {
if (val < min) return min;
if (val > max) return max;
return val;
}
game.down = function (x, y, obj) {
if (gameOver) return;
aiming = true;
handleAim(x, y);
};
game.move = function (x, y, obj) {
if (aiming && !isShooting && !gameOver) {
handleAim(x, y);
}
};
game.up = function (x, y, obj) {
if (aiming && !isShooting && !gameOver) {
launchBubble(aimAngle);
}
aiming = false;
// Remove aim line when shot
if (aimLine && aimLine.parent) {
aimLine.parent.removeChild(aimLine);
aimLine = null;
}
};
function handleAim(x, y) {
// Calculate angle from cannon to (x, y)
var dx = x - cannon.x;
var dy = y - cannon.y;
var angle = Math.atan2(dy, dx);
// Clamp angle to allowed range
angle = clamp(angle, MAX_ANGLE, MIN_ANGLE);
aimAngle = angle;
cannon.setAngle(angle);
// Draw aim line from cannon tip to the aiming point
var startX = cannon.x + Math.cos(angle) * 100;
var startY = cannon.y + Math.sin(angle) * 100;
var endX = cannon.x + Math.cos(angle) * 1200;
var endY = cannon.y + Math.sin(angle) * 1200;
updateAimLine(startX, startY, endX, endY);
}
// --- Game update loop ---
game.update = function () {
// Update flying bubble
if (flyingBubble && flyingBubble.isMoving) {
flyingBubble.update();
// Bounce off walls
if (flyingBubble.x < BUBBLE_RADIUS) {
flyingBubble.x = BUBBLE_RADIUS;
flyingBubble.vx = -flyingBubble.vx;
}
if (flyingBubble.x > GAME_W - BUBBLE_RADIUS) {
flyingBubble.x = GAME_W - BUBBLE_RADIUS;
flyingBubble.vx = -flyingBubble.vx;
}
// Check collision with top
if (flyingBubble.y < GRID_TOP + BUBBLE_RADIUS) {
snapBubbleToGrid(flyingBubble);
} else {
// Check collision with grid bubbles
var hit = false;
for (var row = 0; row < GRID_ROWS; ++row) {
for (var col = 0; col < GRID_COLS; ++col) {
var b = grid[row][col];
if (!b) continue;
var dx = flyingBubble.x - b.x;
var dy = flyingBubble.y - b.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < BUBBLE_DIAM - 2) {
snapBubbleToGrid(flyingBubble);
hit = true;
break;
}
}
if (hit) break;
}
// If bubble falls below bottom, game over
if (flyingBubble && flyingBubble.y > GAME_H - 100) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
gameOver = true;
return;
}
}
}
// Update preview bubble color
if (previewBubble && nextBubbleColor && previewBubble.color !== nextBubbleColor) {
previewBubble.setColor(nextBubbleColor);
}
};