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