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); } 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 ****/ // --- Game constants --- // Bubble colors // Cannon // Bubble shoot sound // Rainbow bubble: white color, acts as all colors // New wall image asset 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; var isAdShowing = false; // Tracks if an ad overlay is active // --- 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) { // Reset sound volume to 1.0 when used in non-bonus context if (popped < 5) { LK.getSound('bubble_pop').volume = 1.0; } 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) { // Check if hitting left or right wall var isWallHit = false; var bestRow = 0, bestCol = 0; if (bubble.x <= BUBBLE_RADIUS + 5) { // Left wall hit - find best row isWallHit = true; var bestDist = 99999; for (var row = 0; row < GRID_ROWS; ++row) { var pos = gridToXY(row, 0); var dy = bubble.y - pos.y; var dist = dy * dy; if (dist < bestDist) { bestDist = dist; bestRow = row; bestCol = 0; } } } else if (bubble.x >= GAME_W - BUBBLE_RADIUS - 5) { // Right wall hit - find best row isWallHit = true; var bestDist = 99999; for (var row = 0; row < GRID_ROWS; ++row) { var pos = gridToXY(row, GRID_COLS - 1); var dy = bubble.y - pos.y; var dist = dy * dy; if (dist < bestDist) { bestDist = dist; bestRow = row; bestCol = GRID_COLS - 1; } } } // If not a wall hit, find nearest empty cell normally if (!isWallHit) { var minDist = 99999; 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 pops the colored bubble it lands on (if any neighbor) and all connected of same color 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') { // Find all connected bubbles of the same color as the neighboring bubble findConnected(n.row, n.col, nb.color, visited); // Also make the rainbow bubble pop itself visited[bestRow * GRID_COLS + bestCol] = true; found = true; break; // Only use one colored neighbor as the seed } } } 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); // Apply bonus multiplier for popping 5+ bubbles at once var bonus = 1.0; if (popped >= 5) { // 5 bubbles = 1.1x, 6 bubbles = 1.2x, etc. bonus = 1.0 + (popped - 4) * 0.1; // Set pop sound volume according to bonus var soundVolume = Math.min(bonus, 2.0); // Cap at 200% volume LK.getSound('bubble_pop').volume = soundVolume; // Show bonus text var bonusTxt = new Text2('BONUS x' + bonus.toFixed(1), { size: 80, fill: 0xFFFF00 }); bonusTxt.anchor.set(0.5, 0.5); bonusTxt.x = bubble.x; bonusTxt.y = bubble.y; game.addChild(bonusTxt); // Animate and remove bonus text tween(bonusTxt, { y: bonusTxt.y - 100, alpha: 0 }, { duration: 1000, onFinish: function onFinish() { if (bonusTxt.parent) bonusTxt.parent.removeChild(bonusTxt); } }); } score += Math.floor(popped * 10 * bonus); 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()) { // --- Spawn a new group of bubbles with different colors --- // Pick a new set of 5 colors different from the previous COLORS var prevColors = COLORS.slice(); var allColors = ['red', 'green', 'blue', 'yellow', 'purple']; // Shuffle and pick a new set that is not the same as previous var newColors = []; var attempts = 0; while (attempts < 10) { // Shuffle allColors var shuffled = allColors.slice(); for (var i = shuffled.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = shuffled[i]; shuffled[i] = shuffled[j]; shuffled[j] = temp; } newColors = shuffled.slice(0, 5); // Check if at least 2 colors are different from previous var diffCount = 0; for (var i = 0; i < 5; ++i) { if (prevColors.indexOf(newColors[i]) === -1) diffCount++; } if (diffCount > 0) break; attempts++; } COLORS = newColors; // Clear the grid and respawn new bubbles for (var row = 0; row < GRID_ROWS; ++row) { for (var col = 0; col < GRID_COLS; ++col) { if (grid[row][col]) { if (grid[row][col].parent) grid[row][col].parent.removeChild(grid[row][col]); grid[row][col] = null; } } } // Fill first 9 rows with new random bubbles for (var row = 0; row < 9; ++row) { for (var col = 0; col < GRID_COLS; ++col) { 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); } } // Prepare next bubble with new color set prepareNextBubble(); gameOver = false; 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 a container for the preview bubble and background var previewContainer = new Container(); previewContainer.x = GAME_W - 470; // Moved another 50 units further left previewContainer.y = GAME_H - 200; // Moved 'centerCircle' (previewBackground in previewContainer) down by another 50 units // Add a circular background behind the preview bubble var previewBackground = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: (100 + 30) / 100 * 2.5, // Increase by 30 units relative to original 100px asset scaleY: (100 + 30) / 100 * 2.5, alpha: 0.3, tint: 0xFFFFFF }); previewContainer.addChild(previewBackground); // Create preview bubble inside the container var previewBubble = new Bubble(); previewBubble.x = 0; // Center in container previewBubble.y = 30; // Move preview bubble down by 10 more units // 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)]); } previewContainer.addChild(previewBubble); game.addChild(previewContainer); // --- Add rainbow bubble button --- var rainbowButton = new Container(); var rainbowBubble = LK.getAsset('bubble_rainbow', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.2, scaleY: 1.2 }); rainbowButton.addChild(rainbowBubble); // Add price text var rainbowPriceTxt = new Text2('500', { size: 60, fill: 0xFFFFFF }); rainbowPriceTxt.anchor.set(0.5, 0.5); rainbowPriceTxt.y = 80; rainbowButton.addChild(rainbowPriceTxt); // Position button on the left side of the screen rainbowButton.x = 120; rainbowButton.y = GAME_H - 120; game.addChild(rainbowButton); // Add button press events rainbowButton.interactive = true; rainbowButton.down = function (x, y, obj) { if (gameOver || isShooting) return; // Check if player has enough points if (score >= 500) { // Subtract points score -= 500; scoreTxt.setText(score); // Set next bubble to rainbow and make it immediately available if (flyingBubble === null) { // If no bubble is flying, make the next bubble rainbow nextBubbleColor = 'rainbow'; previewBubble.setColor(nextBubbleColor); } else { // If a bubble is currently flying, queue up rainbow as the next one nextBubbleColor = 'rainbow'; previewBubble.setColor(nextBubbleColor); } // Visual feedback tween(rainbowButton, { scaleX: 0.8, scaleY: 0.8 }, { duration: 100, onFinish: function onFinish() { tween(rainbowButton, { scaleX: 1, scaleY: 1 }, { duration: 100 }); } }); } else { // Visual feedback for not enough points tween(rainbowPriceTxt, { scaleX: 1.3, scaleY: 1.3, tint: 0xFF0000 }, { duration: 200, onFinish: function onFinish() { tween(rainbowPriceTxt, { scaleX: 1, scaleY: 1, tint: 0xFFFFFF }, { duration: 200 }); } }); } }; // --- Add 'Watch Ad for 250 Score' button --- var adButton = new Container(); // Use the unique 'ad_button' image asset as the button background var adBg = LK.getAsset('ad_button', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5333333333333332, scaleY: 1.5333333333333332, alpha: 1 }); adButton.addChild(adBg); var adTxt = new Text2('Watch Ad\n+250', { size: 35, fill: 0xFFFFFF, align: 'center' }); adTxt.anchor.set(0.5, 0.5); adTxt.y = -170; adButton.addChild(adTxt); // Position ad button to the right of the rainbow button, but not too close to the right edge adButton.x = 125; adButton.y = GAME_H - 370; game.addChild(adButton); adButton.interactive = true; adButton.down = function (x, y, obj) { if (gameOver || isShooting) return; // Show a 10 second "ad" overlay, then give 250 score var overlay = new Container(); var bg = LK.getAsset('ad', { anchorX: 0.5, anchorY: 0.5, width: GAME_W * 0.9, height: GAME_H * 0.9, alpha: 1, x: 0, y: 0 }); overlay.addChild(bg); var adMsg = new Text2('Ad playing...\n(10 seconds)', { size: 90, fill: 0xFFFFFF, align: 'center' }); adMsg.anchor.set(0.5, 0.5); adMsg.x = 0; adMsg.y = -60; overlay.addChild(adMsg); var timerTxt = new Text2('10', { size: 120, fill: 0xFFD700 }); timerTxt.anchor.set(0.5, 0.5); timerTxt.y = 100; overlay.addChild(timerTxt); // Add overlay to LK.gui.center. The overlay's coordinate system origin (top-left) // will now be relative to the screen's center point. LK.gui.center.addChild(overlay); isAdShowing = true; // Disable input while ad is showing // Position the overlay's top-left corner at (0,0) relative to LK.gui.center. // This effectively places the overlay's origin at the screen center. overlay.x = 0; overlay.y = 0; // Since the ad background 'bg' is centered within 'overlay' (anchor 0.5,0.5 at overlay's 0,0), // and text elements are also centered horizontally within 'overlay', the entire ad display // will now appear centered on the screen. var secondsLeft = 10; var adTimer = null; function tickAdTimer() { secondsLeft--; timerTxt.setText(secondsLeft.toString()); if (secondsLeft <= 0) { LK.clearInterval(adTimer); // Remove overlay if (overlay.parent) overlay.parent.removeChild(overlay); isAdShowing = false; // Re-enable input after ad // Give 250 score score += 250; scoreTxt.setText(score); // Visual feedback tween(adButton, { scaleX: 1.2, scaleY: 1.2 }, { duration: 120, onFinish: function onFinish() { tween(adButton, { scaleX: 1, scaleY: 1 }, { duration: 120 }); } }); } } timerTxt.setText(secondsLeft.toString()); adTimer = LK.setInterval(tickAdTimer, 1000); }; // --- 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); // Create a continuous line effect using the bubble color that matches nextBubbleColor var lineColor = nextBubbleColor || 'blue'; var lineBaseAsset = lineColor === 'rainbow' ? 'bubble_rainbow' : 'bubble_' + lineColor; 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 bubble of the current color as a dot var dot = LK.getAsset(lineBaseAsset, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.18, scaleY: 0.18, alpha: 0.35 + (i === 0 ? 0.2 : 0) // Make first dot slightly more visible }); dot.x = px; dot.y = py; // Add subtle animation to the dots tween(dot, { alpha: dot.alpha - 0.1, scaleX: dot.scaleX - 0.03, scaleY: dot.scaleY - 0.03 }, { duration: 300 + i * 10, easing: tween.easeOut }); 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 || isAdShowing) return; aiming = true; handleAim(x, y); }; game.move = function (x, y, obj) { if (isAdShowing) return; // Prevent input during ad if (aiming && !isShooting && !gameOver) { handleAim(x, y); } }; game.up = function (x, y, obj) { if (isAdShowing) return; // Prevent input during ad 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(); // Stick to walls instead of bouncing if (flyingBubble.x < BUBBLE_RADIUS) { // Stick to left wall flyingBubble.x = BUBBLE_RADIUS; snapBubbleToGrid(flyingBubble); } if (flyingBubble.x > GAME_W - BUBBLE_RADIUS) { // Stick to right wall flyingBubble.x = GAME_W - BUBBLE_RADIUS; snapBubbleToGrid(flyingBubble); } // 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
****/
// --- Game constants ---
// Bubble colors
// Cannon
// Bubble shoot sound
// Rainbow bubble: white color, acts as all colors
// New wall image asset
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;
var isAdShowing = false; // Tracks if an ad overlay is active
// --- 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) {
// Reset sound volume to 1.0 when used in non-bonus context
if (popped < 5) {
LK.getSound('bubble_pop').volume = 1.0;
}
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) {
// Check if hitting left or right wall
var isWallHit = false;
var bestRow = 0,
bestCol = 0;
if (bubble.x <= BUBBLE_RADIUS + 5) {
// Left wall hit - find best row
isWallHit = true;
var bestDist = 99999;
for (var row = 0; row < GRID_ROWS; ++row) {
var pos = gridToXY(row, 0);
var dy = bubble.y - pos.y;
var dist = dy * dy;
if (dist < bestDist) {
bestDist = dist;
bestRow = row;
bestCol = 0;
}
}
} else if (bubble.x >= GAME_W - BUBBLE_RADIUS - 5) {
// Right wall hit - find best row
isWallHit = true;
var bestDist = 99999;
for (var row = 0; row < GRID_ROWS; ++row) {
var pos = gridToXY(row, GRID_COLS - 1);
var dy = bubble.y - pos.y;
var dist = dy * dy;
if (dist < bestDist) {
bestDist = dist;
bestRow = row;
bestCol = GRID_COLS - 1;
}
}
}
// If not a wall hit, find nearest empty cell normally
if (!isWallHit) {
var minDist = 99999;
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 pops the colored bubble it lands on (if any neighbor) and all connected of same color
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') {
// Find all connected bubbles of the same color as the neighboring bubble
findConnected(n.row, n.col, nb.color, visited);
// Also make the rainbow bubble pop itself
visited[bestRow * GRID_COLS + bestCol] = true;
found = true;
break; // Only use one colored neighbor as the seed
}
}
} 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);
// Apply bonus multiplier for popping 5+ bubbles at once
var bonus = 1.0;
if (popped >= 5) {
// 5 bubbles = 1.1x, 6 bubbles = 1.2x, etc.
bonus = 1.0 + (popped - 4) * 0.1;
// Set pop sound volume according to bonus
var soundVolume = Math.min(bonus, 2.0); // Cap at 200% volume
LK.getSound('bubble_pop').volume = soundVolume;
// Show bonus text
var bonusTxt = new Text2('BONUS x' + bonus.toFixed(1), {
size: 80,
fill: 0xFFFF00
});
bonusTxt.anchor.set(0.5, 0.5);
bonusTxt.x = bubble.x;
bonusTxt.y = bubble.y;
game.addChild(bonusTxt);
// Animate and remove bonus text
tween(bonusTxt, {
y: bonusTxt.y - 100,
alpha: 0
}, {
duration: 1000,
onFinish: function onFinish() {
if (bonusTxt.parent) bonusTxt.parent.removeChild(bonusTxt);
}
});
}
score += Math.floor(popped * 10 * bonus);
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()) {
// --- Spawn a new group of bubbles with different colors ---
// Pick a new set of 5 colors different from the previous COLORS
var prevColors = COLORS.slice();
var allColors = ['red', 'green', 'blue', 'yellow', 'purple'];
// Shuffle and pick a new set that is not the same as previous
var newColors = [];
var attempts = 0;
while (attempts < 10) {
// Shuffle allColors
var shuffled = allColors.slice();
for (var i = shuffled.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = shuffled[i];
shuffled[i] = shuffled[j];
shuffled[j] = temp;
}
newColors = shuffled.slice(0, 5);
// Check if at least 2 colors are different from previous
var diffCount = 0;
for (var i = 0; i < 5; ++i) {
if (prevColors.indexOf(newColors[i]) === -1) diffCount++;
}
if (diffCount > 0) break;
attempts++;
}
COLORS = newColors;
// Clear the grid and respawn new bubbles
for (var row = 0; row < GRID_ROWS; ++row) {
for (var col = 0; col < GRID_COLS; ++col) {
if (grid[row][col]) {
if (grid[row][col].parent) grid[row][col].parent.removeChild(grid[row][col]);
grid[row][col] = null;
}
}
}
// Fill first 9 rows with new random bubbles
for (var row = 0; row < 9; ++row) {
for (var col = 0; col < GRID_COLS; ++col) {
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);
}
}
// Prepare next bubble with new color set
prepareNextBubble();
gameOver = false;
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 a container for the preview bubble and background
var previewContainer = new Container();
previewContainer.x = GAME_W - 470; // Moved another 50 units further left
previewContainer.y = GAME_H - 200; // Moved 'centerCircle' (previewBackground in previewContainer) down by another 50 units
// Add a circular background behind the preview bubble
var previewBackground = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: (100 + 30) / 100 * 2.5,
// Increase by 30 units relative to original 100px asset
scaleY: (100 + 30) / 100 * 2.5,
alpha: 0.3,
tint: 0xFFFFFF
});
previewContainer.addChild(previewBackground);
// Create preview bubble inside the container
var previewBubble = new Bubble();
previewBubble.x = 0; // Center in container
previewBubble.y = 30; // Move preview bubble down by 10 more units
// 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)]);
}
previewContainer.addChild(previewBubble);
game.addChild(previewContainer);
// --- Add rainbow bubble button ---
var rainbowButton = new Container();
var rainbowBubble = LK.getAsset('bubble_rainbow', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2
});
rainbowButton.addChild(rainbowBubble);
// Add price text
var rainbowPriceTxt = new Text2('500', {
size: 60,
fill: 0xFFFFFF
});
rainbowPriceTxt.anchor.set(0.5, 0.5);
rainbowPriceTxt.y = 80;
rainbowButton.addChild(rainbowPriceTxt);
// Position button on the left side of the screen
rainbowButton.x = 120;
rainbowButton.y = GAME_H - 120;
game.addChild(rainbowButton);
// Add button press events
rainbowButton.interactive = true;
rainbowButton.down = function (x, y, obj) {
if (gameOver || isShooting) return;
// Check if player has enough points
if (score >= 500) {
// Subtract points
score -= 500;
scoreTxt.setText(score);
// Set next bubble to rainbow and make it immediately available
if (flyingBubble === null) {
// If no bubble is flying, make the next bubble rainbow
nextBubbleColor = 'rainbow';
previewBubble.setColor(nextBubbleColor);
} else {
// If a bubble is currently flying, queue up rainbow as the next one
nextBubbleColor = 'rainbow';
previewBubble.setColor(nextBubbleColor);
}
// Visual feedback
tween(rainbowButton, {
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 100,
onFinish: function onFinish() {
tween(rainbowButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
}
});
} else {
// Visual feedback for not enough points
tween(rainbowPriceTxt, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFF0000
}, {
duration: 200,
onFinish: function onFinish() {
tween(rainbowPriceTxt, {
scaleX: 1,
scaleY: 1,
tint: 0xFFFFFF
}, {
duration: 200
});
}
});
}
};
// --- Add 'Watch Ad for 250 Score' button ---
var adButton = new Container();
// Use the unique 'ad_button' image asset as the button background
var adBg = LK.getAsset('ad_button', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5333333333333332,
scaleY: 1.5333333333333332,
alpha: 1
});
adButton.addChild(adBg);
var adTxt = new Text2('Watch Ad\n+250', {
size: 35,
fill: 0xFFFFFF,
align: 'center'
});
adTxt.anchor.set(0.5, 0.5);
adTxt.y = -170;
adButton.addChild(adTxt);
// Position ad button to the right of the rainbow button, but not too close to the right edge
adButton.x = 125;
adButton.y = GAME_H - 370;
game.addChild(adButton);
adButton.interactive = true;
adButton.down = function (x, y, obj) {
if (gameOver || isShooting) return;
// Show a 10 second "ad" overlay, then give 250 score
var overlay = new Container();
var bg = LK.getAsset('ad', {
anchorX: 0.5,
anchorY: 0.5,
width: GAME_W * 0.9,
height: GAME_H * 0.9,
alpha: 1,
x: 0,
y: 0
});
overlay.addChild(bg);
var adMsg = new Text2('Ad playing...\n(10 seconds)', {
size: 90,
fill: 0xFFFFFF,
align: 'center'
});
adMsg.anchor.set(0.5, 0.5);
adMsg.x = 0;
adMsg.y = -60;
overlay.addChild(adMsg);
var timerTxt = new Text2('10', {
size: 120,
fill: 0xFFD700
});
timerTxt.anchor.set(0.5, 0.5);
timerTxt.y = 100;
overlay.addChild(timerTxt);
// Add overlay to LK.gui.center. The overlay's coordinate system origin (top-left)
// will now be relative to the screen's center point.
LK.gui.center.addChild(overlay);
isAdShowing = true; // Disable input while ad is showing
// Position the overlay's top-left corner at (0,0) relative to LK.gui.center.
// This effectively places the overlay's origin at the screen center.
overlay.x = 0;
overlay.y = 0;
// Since the ad background 'bg' is centered within 'overlay' (anchor 0.5,0.5 at overlay's 0,0),
// and text elements are also centered horizontally within 'overlay', the entire ad display
// will now appear centered on the screen.
var secondsLeft = 10;
var adTimer = null;
function tickAdTimer() {
secondsLeft--;
timerTxt.setText(secondsLeft.toString());
if (secondsLeft <= 0) {
LK.clearInterval(adTimer);
// Remove overlay
if (overlay.parent) overlay.parent.removeChild(overlay);
isAdShowing = false; // Re-enable input after ad
// Give 250 score
score += 250;
scoreTxt.setText(score);
// Visual feedback
tween(adButton, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
onFinish: function onFinish() {
tween(adButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 120
});
}
});
}
}
timerTxt.setText(secondsLeft.toString());
adTimer = LK.setInterval(tickAdTimer, 1000);
};
// --- 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);
// Create a continuous line effect using the bubble color that matches nextBubbleColor
var lineColor = nextBubbleColor || 'blue';
var lineBaseAsset = lineColor === 'rainbow' ? 'bubble_rainbow' : 'bubble_' + lineColor;
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 bubble of the current color as a dot
var dot = LK.getAsset(lineBaseAsset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.18,
scaleY: 0.18,
alpha: 0.35 + (i === 0 ? 0.2 : 0) // Make first dot slightly more visible
});
dot.x = px;
dot.y = py;
// Add subtle animation to the dots
tween(dot, {
alpha: dot.alpha - 0.1,
scaleX: dot.scaleX - 0.03,
scaleY: dot.scaleY - 0.03
}, {
duration: 300 + i * 10,
easing: tween.easeOut
});
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 || isAdShowing) return;
aiming = true;
handleAim(x, y);
};
game.move = function (x, y, obj) {
if (isAdShowing) return; // Prevent input during ad
if (aiming && !isShooting && !gameOver) {
handleAim(x, y);
}
};
game.up = function (x, y, obj) {
if (isAdShowing) return; // Prevent input during ad
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();
// Stick to walls instead of bouncing
if (flyingBubble.x < BUBBLE_RADIUS) {
// Stick to left wall
flyingBubble.x = BUBBLE_RADIUS;
snapBubbleToGrid(flyingBubble);
}
if (flyingBubble.x > GAME_W - BUBBLE_RADIUS) {
// Stick to right wall
flyingBubble.x = GAME_W - BUBBLE_RADIUS;
snapBubbleToGrid(flyingBubble);
}
// 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);
}
};