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