/**** * 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);
}
};