User prompt
Move the preview bubble left by another 50 units
User prompt
Make the game have 3 more layers of bubbles
User prompt
Make the walls wider by 200 units
User prompt
move it again by 50 units
User prompt
move the preview bubbles to the left by 50 units
User prompt
continue your thought process, and ensure preview bubble is always visible in update function.
User prompt
make the current bubble that is ready to be shot be in the bottom right always which means during the game, during winning, during losing. Not just when you aren’t playing.
User prompt
MAKE THE CURRENT BUBBLE THAT SHOULD APPEAR IN THE BOTTOM RIGHT ALWAYS EXIST.
User prompt
Make the bubble in the bottom right always show, not just after the game.
User prompt
Make it so that the current bubble being shot is shown in the bottom right
User prompt
Make a new image and make the walls display that image
User prompt
Make it so the rainbow bubbles cant make you win and only pop the coloured bubble it lands on
User prompt
Change the rainbow bubbles to only spawn as a shootable bubble
User prompt
Create a rainbow bubble that is very rare and acts as all the colours and can be used to clear the colours
User prompt
Make the boxes on the sides stretch all the way down to the bottom
User prompt
Make it so there are boxes on the left and right sides so the bubbles cant go out
User prompt
Draw a line from the cannon to the area that is currently being touched
User prompt
Make it so you can still hold and when you release input, that’s when you shoot the bubble
User prompt
Make it so that you can tap anywhere to aim the bubble shooter
Code edit (1 edits merged)
Please save this source code
User prompt
Bubble Pop Master
Initial prompt
A bubble shooter style game like the ones where you sit at the bottom middle and shoot coloured bubbles to the same colour to knock those coloured bubbles down
/****
* 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);
}
self.bubbleAsset = self.attachAsset('bubble_' + color, {
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
****/
// 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 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];
if (!b || 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 6 rows with random bubbles
for (var row = 0; row < 6; ++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() {
nextBubbleColor = getRandomColorOnBoard();
}
// 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 = {};
findConnected(bestRow, bestCol, bubble.color, visited);
// Count connected
var count = 0;
for (var k in visited) if (visited[k]) count++;
if (count >= 3) {
// Remove connected
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 (checkWin()) {
LK.showYouWin();
gameOver = true;
return;
}
if (checkGameOver()) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
gameOver = true;
return;
}
}
// --- Game initialization ---
initGrid();
initCannon();
prepareNextBubble();
// --- Draw next bubble preview ---
var previewBubble = new Bubble();
previewBubble.setColor(nextBubbleColor);
previewBubble.x = GAME_W - 200;
previewBubble.y = GAME_H - 200;
game.addChild(previewBubble);
// --- Input handling ---
// Drag to aim
var aiming = false;
var aimAngle = -Math.PI / 2; // Up
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;
// Only allow aiming in lower half of screen
if (y > GAME_H / 2) {
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) {
// Shoot!
launchBubble(aimAngle);
aiming = false;
}
};
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);
}
// --- 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.setColor(nextBubbleColor);
}
}; ===================================================================
--- original.js
+++ change.js
@@ -1,6 +1,507 @@
-/****
+/****
+* 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);
+ }
+ self.bubbleAsset = self.attachAsset('bubble_' + color, {
+ 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: 0x000000
-});
\ No newline at end of file
+ backgroundColor: 0x222244
+});
+
+/****
+* Game Code
+****/
+// 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 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];
+ if (!b || 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 6 rows with random bubbles
+ for (var row = 0; row < 6; ++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() {
+ nextBubbleColor = getRandomColorOnBoard();
+}
+// 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 = {};
+ findConnected(bestRow, bestCol, bubble.color, visited);
+ // Count connected
+ var count = 0;
+ for (var k in visited) if (visited[k]) count++;
+ if (count >= 3) {
+ // Remove connected
+ 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 (checkWin()) {
+ LK.showYouWin();
+ gameOver = true;
+ return;
+ }
+ if (checkGameOver()) {
+ LK.effects.flashScreen(0xff0000, 1000);
+ LK.showGameOver();
+ gameOver = true;
+ return;
+ }
+}
+// --- Game initialization ---
+initGrid();
+initCannon();
+prepareNextBubble();
+// --- Draw next bubble preview ---
+var previewBubble = new Bubble();
+previewBubble.setColor(nextBubbleColor);
+previewBubble.x = GAME_W - 200;
+previewBubble.y = GAME_H - 200;
+game.addChild(previewBubble);
+// --- Input handling ---
+// Drag to aim
+var aiming = false;
+var aimAngle = -Math.PI / 2; // Up
+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;
+ // Only allow aiming in lower half of screen
+ if (y > GAME_H / 2) {
+ 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) {
+ // Shoot!
+ launchBubble(aimAngle);
+ aiming = false;
+ }
+};
+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);
+}
+// --- 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.setColor(nextBubbleColor);
+ }
+};
\ No newline at end of file