/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Bubble class for grid bubbles var Bubble = Container.expand(function () { var self = Container.call(this); // color: string, e.g. 'red' self.init = function (color) { self.color = color; self.assetId = 'bubble_' + color; self.asset = self.attachAsset(self.assetId, { anchorX: 0.5, anchorY: 0.5 }); self.radius = self.asset.width / 2; self.gridRow = 0; self.gridCol = 0; self.popped = false; }; // Pop animation self.pop = function (_onFinish) { if (self.popped) return; self.popped = true; tween(self.asset, { scaleX: 1.3, scaleY: 1.3, alpha: 0 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); if (_onFinish) _onFinish(); } }); LK.getSound('pop').play(); }; return self; }); // Cannon class var Cannon = Container.expand(function () { var self = Container.call(this); self.asset = self.attachAsset('cannon', { anchorX: 0.5, anchorY: 0.5 }); self.angle = -Math.PI / 2; // Upwards // Set angle (radians) self.setAngle = function (a) { // Clamp angle between -80deg and -100deg (left/right) var minA = -Math.PI * 5 / 6; // -150deg var maxA = -Math.PI / 6; // -30deg if (a < minA) a = minA; if (a > maxA) a = maxA; self.angle = a; self.asset.rotation = a + Math.PI / 2; }; return self; }); // Shot bubble class var ShotBubble = Container.expand(function () { var self = Container.call(this); // color: string, angle: radians self.init = function (color, angle) { self.color = color; self.assetId = 'bubble_shot_' + color; self.asset = self.attachAsset(self.assetId, { anchorX: 0.5, anchorY: 0.5 }); self.radius = self.asset.width / 2; self.angle = angle; self.speed = 38; // px per tick self.stuck = false; }; // Move bubble self.update = function () { if (self.stuck) return; self.x += Math.cos(self.angle) * self.speed; self.y += Math.sin(self.angle) * self.speed; }; // Stick bubble (stop movement) self.stick = function () { self.stuck = true; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222a38 }); /**** * Game Code ****/ // LK.init.music('bgmusic', {volume: 1}); // Music (optional, not played here) // Sound // Bubble shot // Cannon // Bubble colors // --- Game constants --- // --- Bubble asset variants grouped for easy management --- // --- Shot bubble asset variants --- // --- Other assets --- // Background image asset (full screen) var GAME_W = 2048; var GAME_H = 2732; var GRID_COLS = 10; var GRID_ROWS = 12; var BUBBLE_SIZE = 120; // px var GRID_TOP = 200; var GRID_LEFT = (GAME_W - GRID_COLS * BUBBLE_SIZE) / 2; var GRID_BOTTOM = GRID_TOP + GRID_ROWS * BUBBLE_SIZE; var COLORS = ['red', 'blue', 'green', 'yellow', 'purple', 'orange']; var START_COLORS = 3; // Start with 3 colors, increase as XP increases var XP_PER_BUBBLE = 10; // --- Game state --- var grid = []; // 2D array [row][col] of Bubble or null var bubbles = []; // All Bubble instances var shotBubbles = []; // All ShotBubble instances var cannon; var nextBubbleColor = null; var canShoot = true; var score = 0; var xp = 0; var level = 1; var lastTouchX = 0, lastTouchY = 0; var dragAiming = false; var gameOver = false; // --- UI --- var scoreTxt = new Text2('XP: 0', { size: 100, fill: "#fff", font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" // Use a bolder font }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var levelTxt = new Text2('Level 1', { size: 60, fill: "#fff", font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" // Use a bolder font }); levelTxt.anchor.set(0.5, 0); LK.gui.top.addChild(levelTxt); levelTxt.y = 110; // --- Helper functions --- // Get color for new bubble (based on current grid) function getRandomColor() { // Only use colors present in grid, or up to current level var available = {}; for (var r = 0; r < GRID_ROWS; r++) { for (var c = 0; c < GRID_COLS; c++) { var b = grid[r][c]; if (b && !available[b.color]) available[b.color] = true; } } var colorList = []; for (var i = 0; i < START_COLORS + level - 1 && i < COLORS.length; i++) colorList.push(COLORS[i]); var gridColors = []; for (var k in available) gridColors.push(k); var useColors = gridColors.length ? gridColors : colorList; return useColors[Math.floor(Math.random() * useColors.length)]; } // Convert grid row,col to x,y function gridToXY(row, col) { var x = GRID_LEFT + col * BUBBLE_SIZE + BUBBLE_SIZE / 2; var y = GRID_TOP + row * BUBBLE_SIZE + BUBBLE_SIZE / 2; return { x: x, y: y }; } // Convert x,y to grid row,col function xyToGrid(x, y) { var col = Math.floor((x - GRID_LEFT) / BUBBLE_SIZE); var row = Math.floor((y - GRID_TOP) / BUBBLE_SIZE); return { row: row, col: col }; } // Check if row,col is in grid function inGrid(row, col) { return row >= 0 && row < GRID_ROWS && col >= 0 && col < GRID_COLS; } // Find all connected bubbles of same color (flood fill) function findConnected(row, col, color, visited) { if (!inGrid(row, col)) return []; if (!grid[row][col]) return []; if (grid[row][col].color !== color) return []; var key = row + ',' + col; if (visited[key]) return []; visited[key] = true; var found = [[row, col]]; // 4-way neighbors var dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]]; for (var i = 0; i < dirs.length; i++) { var nr = row + dirs[i][0], nc = col + dirs[i][1]; if (inGrid(nr, nc)) { var more = findConnected(nr, nc, color, visited); for (var j = 0; j < more.length; j++) found.push(more[j]); } } return found; } // Remove bubbles at positions function popBubbles(positions) { for (var i = 0; i < positions.length; i++) { var rc = positions[i]; var b = grid[rc[0]][rc[1]]; if (b) { b.pop(); grid[rc[0]][rc[1]] = null; } } // Check for win condition: all bubbles cleared in level 2 if (level === 2) { var anyBubble = false; for (var r = 0; r < GRID_ROWS; r++) { for (var c = 0; c < GRID_COLS; c++) { if (grid[r][c]) { anyBubble = true; break; } } if (anyBubble) break; } if (!anyBubble) { // All bubbles cleared, show you win LK.effects.flashScreen(0x00ff00, 1000); LK.showYouWin(); gameOver = true; } } } // Drop floating bubbles (not connected to top) function dropFloatingBubbles() { var connected = {}; // Mark all bubbles connected to top row function markConnected(row, col) { var key = row + ',' + col; if (!inGrid(row, col)) return; if (!grid[row][col]) return; if (connected[key]) return; connected[key] = true; var dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]]; for (var i = 0; i < dirs.length; i++) { var nr = row + dirs[i][0], nc = col + dirs[i][1]; markConnected(nr, nc); } } for (var c = 0; c < GRID_COLS; c++) { if (grid[0][c]) markConnected(0, c); } // Any bubble not in connected is floating for (var r = 0; r < GRID_ROWS; r++) { for (var c = 0; c < GRID_COLS; c++) { var key = r + ',' + c; if (grid[r][c] && !connected[key]) { grid[r][c].pop(); grid[r][c] = null; } } } } // Check for game over (bubbles at bottom row) function checkGameOver() { for (var c = 0; c < GRID_COLS; c++) { if (grid[GRID_ROWS - 1][c]) { gameOver = true; LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); return true; } } return false; } // Add XP and update UI function addXP(amount) { xp += amount; scoreTxt.setText('XP: ' + xp); // Duplicate the player (cannon) for help every time XP is gained if (typeof cannons === "undefined") { cannons = [cannon]; } var newCannon = new Cannon(); newCannon.x = GAME_W / 2 + cannons.length * 120 - 60 * (cannons.length % 2 === 0 ? 1 : -1); newCannon.y = GAME_H - 180; newCannon.setAngle(-Math.PI / 2); game.addChild(newCannon); cannons.push(newCannon); // --- You Win at 1000 XP --- if (xp >= 1000 && !gameOver) { gameOver = true; // Show a custom you win overlay with a respawn button var overlay = new Container(); overlay.x = 0; overlay.y = 0; overlay.width = GAME_W; overlay.height = GAME_H; // Dim background var bgRect = LK.getAsset('background', { anchorX: 0, anchorY: 0, x: 0, y: 0, alpha: 0.7 }); overlay.addChild(bgRect); // "You Win!" text var winTxt = new Text2('You Win!', { size: 200, fill: "#fff", font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" // Use a bolder font }); winTxt.anchor.set(0.5, 0.5); winTxt.x = GAME_W / 2; winTxt.y = GAME_H / 2 - 200; overlay.addChild(winTxt); // Respawn button var btnW = 600, btnH = 180; var btn = LK.getAsset('bubble_green', { anchorX: 0.5, anchorY: 0.5, x: GAME_W / 2, y: GAME_H / 2 + 100, scaleX: btnW / 120, scaleY: btnH / 120 }); overlay.addChild(btn); var btnTxt = new Text2('Respawn', { size: 100, fill: "#222", font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" // Use a bolder font }); btnTxt.anchor.set(0.5, 0.5); btnTxt.x = GAME_W / 2; btnTxt.y = GAME_H / 2 + 100; overlay.addChild(btnTxt); // Button interaction btn.down = function (x, y, obj) { // Remove overlay if (overlay.parent) overlay.parent.removeChild(overlay); // Restart game startGame(); }; // Add overlay to game game.addChild(overlay); // Prevent further XP gain or actions return; } // Level up every 200 XP var newLevel = 1 + Math.floor(xp / 200); if (newLevel > level) { level = newLevel; levelTxt.setText('Level ' + level); if (level === 2) { // Start level 2: reset grid with more balls // Remove all bubbles from game for (var r = 0; r < GRID_ROWS; r++) { for (var c = 0; c < GRID_COLS; c++) { if (grid[r][c]) { grid[r][c].destroy(); grid[r][c] = null; } } } // Remove all shot bubbles for (var i = 0; i < shotBubbles.length; i++) { if (shotBubbles[i]) shotBubbles[i].destroy(); } shotBubbles = []; // Reset cannons to just one for (var i = 1; i < cannons.length; i++) { if (cannons[i]) cannons[i].destroy(); } cannons = [cannon]; // Re-initialize grid for level 2 initGrid(); pickNextBubbleColor(); canShoot = true; gameOver = false; } else { // Add new color if possible if (START_COLORS + level - 1 <= COLORS.length) { // Add a row of new color bubbles at top addNewRow(); } } } } // Add a new row of bubbles at the top (for level up) function addNewRow() { // Shift all rows down for (var r = GRID_ROWS - 1; r > 0; r--) { for (var c = 0; c < GRID_COLS; c++) { if (grid[r - 1][c]) { grid[r][c] = grid[r - 1][c]; grid[r][c].gridRow = r; var pos = gridToXY(r, c); tween(grid[r][c], { x: pos.x, y: pos.y }, { duration: 200, easing: tween.easeOut }); } else { grid[r][c] = null; } } } // Add new row var colorIdx = Math.min(START_COLORS + level - 2, COLORS.length - 1); var color = COLORS[colorIdx]; for (var c = 0; c < GRID_COLS; c++) { var b = new Bubble(); b.init(color); var pos = gridToXY(0, c); b.x = pos.x; b.y = pos.y; b.gridRow = 0; b.gridCol = c; grid[0][c] = b; game.addChild(b); } } // --- Game setup --- // Initialize grid function initGrid() { grid = []; var fillRows = 8; if (level === 2) { fillRows = 12; // Fill all rows for level 2 } for (var r = 0; r < GRID_ROWS; r++) { grid[r] = []; for (var c = 0; c < GRID_COLS; c++) { if (r < fillRows) { // Fill top rows with random color var colorIdx = Math.floor(Math.random() * (START_COLORS + level - 1)); var color = COLORS[colorIdx]; var b = new Bubble(); b.init(color); var pos = gridToXY(r, c); b.x = pos.x; b.y = pos.y; b.gridRow = r; b.gridCol = c; grid[r][c] = b; game.addChild(b); } else { grid[r][c] = null; } } } } // Initialize cannon function initCannon() { cannon = new Cannon(); cannon.x = GAME_W / 2; cannon.y = GAME_H - 180; cannon.setAngle(-Math.PI / 2); game.addChild(cannon); } // Prepare next bubble color function pickNextBubbleColor() { nextBubbleColor = getRandomColor(); } // Shoot a bubble function shootBubble(angle) { if (!canShoot || gameOver) return; canShoot = false; // If cannons array is not defined, fallback to single cannon if (typeof cannons === "undefined" || !Array.isArray(cannons)) { cannons = [cannon]; } // Each cannon shoots a bubble for (var i = 0; i < cannons.length; i++) { var c = cannons[i]; var b = new ShotBubble(); b.init(nextBubbleColor, c.angle); b.x = c.x; b.y = c.y - 60; shotBubbles.push(b); game.addChild(b); LK.getSound('shoot').play(); } pickNextBubbleColor(); // Allow next shot after short delay LK.setTimeout(function () { canShoot = true; }, 300); } // --- Input handling --- // Aim cannon with drag/tap function aimAt(x, y) { // Clamp to avoid aiming below horizontal var dx = x - cannon.x; var dy = y - cannon.y; var angle = Math.atan2(dy, dx); cannon.setAngle(angle); lastTouchX = x; lastTouchY = y; } // Shoot on release function tryShoot() { if (!canShoot || gameOver) return; shootBubble(cannon.angle); } // Touch/drag events game.down = function (x, y, obj) { if (gameOver) return; if (y > GAME_H - 500) { dragAiming = true; aimAt(x, y); } }; game.move = function (x, y, obj) { if (dragAiming && !gameOver) { aimAt(x, y); // --- Swipe to crush 4+ adjacent bubbles --- // Convert to grid coordinates var pos = xyToGrid(x, y); var row = pos.row, col = pos.col; if (inGrid(row, col)) { var gb = grid[row][col]; if (gb && !gb.popped && gb.asset && gb.asset._wasSwiped !== true) { // Find all connected bubbles of same color var connected = findConnected(row, col, gb.color, {}); if (connected.length >= 4) { // Mark all as swiped to prevent double pop in one swipe for (var i = 0; i < connected.length; i++) { var rc = connected[i]; if (grid[rc[0]][rc[1]] && grid[rc[0]][rc[1]].asset) { grid[rc[0]][rc[1]].asset._wasSwiped = true; } } popBubbles(connected); addXP(connected.length * XP_PER_BUBBLE); dropFloatingBubbles(); } } } } }; game.up = function (x, y, obj) { if (dragAiming && !gameOver) { tryShoot(); dragAiming = false; } }; // --- Main update loop --- game.update = function () { // Update shot bubbles for (var i = shotBubbles.length - 1; i >= 0; i--) { var b = shotBubbles[i]; b.update(); // Out of bounds if (b.x < 0 || b.x > GAME_W || b.y < 0) { b.destroy(); shotBubbles.splice(i, 1); continue; } // Collision with grid var hit = false; for (var r = 0; r < GRID_ROWS; r++) { for (var c = 0; c < GRID_COLS; c++) { var gb = grid[r][c]; if (gb) { var dx = b.x - gb.x; var dy = b.y - gb.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < BUBBLE_SIZE - 10) { // Stick bubble to grid var pos = xyToGrid(b.x, b.y); var row = pos.row, col = pos.col; if (!inGrid(row, col)) { // Out of grid, game over b.destroy(); shotBubbles.splice(i, 1); checkGameOver(); return; } // Find nearest empty cell if (grid[row][col]) { // Try above if (row > 0 && !grid[row - 1][col]) row--;else if (col > 0 && !grid[row][col - 1]) col--;else if (col < GRID_COLS - 1 && !grid[row][col + 1]) col++; } // Place bubble b.stick(); var pos2 = gridToXY(row, col); tween(b, { x: pos2.x, y: pos2.y }, { duration: 80, easing: tween.easeOut, onFinish: function onFinish() {} }); b.gridRow = row; b.gridCol = col; // Convert to grid bubble var newB = new Bubble(); newB.init(b.color); newB.x = pos2.x; newB.y = pos2.y; newB.gridRow = row; newB.gridCol = col; grid[row][col] = newB; game.addChild(newB); // Remove shot bubble b.destroy(); shotBubbles.splice(i, 1); // Check for matches var connected = findConnected(row, col, newB.color, {}); if (connected.length >= 3) { popBubbles(connected); addXP(connected.length * XP_PER_BUBBLE); dropFloatingBubbles(); } checkGameOver(); hit = true; break; } } } if (hit) break; } } // --- Tap to pop grid bubbles and earn XP --- for (var r = 0; r < GRID_ROWS; r++) { for (var c = 0; c < GRID_COLS; c++) { var gb = grid[r][c]; if (gb && !gb.popped && gb.asset && gb.asset._wasDown !== true && gb.asset._downListenerSet !== true) { // Set a one-time down listener for this bubble gb.asset._downListenerSet = true; (function (gb, r, c) { gb.asset.down = function (x, y, obj) { if (gb.popped) return; gb.pop(); grid[r][c] = null; addXP(XP_PER_BUBBLE); }; })(gb, r, c); } } } }; // --- Game start --- function startGame() { xp = 0; level = 1; scoreTxt.setText('XP: 0'); levelTxt.setText('Level 1'); gameOver = false; shotBubbles = []; canShoot = true; // Add background image to the game scene var bg = LK.getAsset('background', { anchorX: 0, anchorY: 0, x: 0, y: 0 }); game.addChildAt(bg, 0); initGrid(); initCannon(); pickNextBubbleColor(); } startGame(); ; LK.playMusic('Space');
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Bubble class for grid bubbles
var Bubble = Container.expand(function () {
var self = Container.call(this);
// color: string, e.g. 'red'
self.init = function (color) {
self.color = color;
self.assetId = 'bubble_' + color;
self.asset = self.attachAsset(self.assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.asset.width / 2;
self.gridRow = 0;
self.gridCol = 0;
self.popped = false;
};
// Pop animation
self.pop = function (_onFinish) {
if (self.popped) return;
self.popped = true;
tween(self.asset, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
if (_onFinish) _onFinish();
}
});
LK.getSound('pop').play();
};
return self;
});
// Cannon class
var Cannon = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('cannon', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = -Math.PI / 2; // Upwards
// Set angle (radians)
self.setAngle = function (a) {
// Clamp angle between -80deg and -100deg (left/right)
var minA = -Math.PI * 5 / 6; // -150deg
var maxA = -Math.PI / 6; // -30deg
if (a < minA) a = minA;
if (a > maxA) a = maxA;
self.angle = a;
self.asset.rotation = a + Math.PI / 2;
};
return self;
});
// Shot bubble class
var ShotBubble = Container.expand(function () {
var self = Container.call(this);
// color: string, angle: radians
self.init = function (color, angle) {
self.color = color;
self.assetId = 'bubble_shot_' + color;
self.asset = self.attachAsset(self.assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.asset.width / 2;
self.angle = angle;
self.speed = 38; // px per tick
self.stuck = false;
};
// Move bubble
self.update = function () {
if (self.stuck) return;
self.x += Math.cos(self.angle) * self.speed;
self.y += Math.sin(self.angle) * self.speed;
};
// Stick bubble (stop movement)
self.stick = function () {
self.stuck = true;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222a38
});
/****
* Game Code
****/
// LK.init.music('bgmusic', {volume: 1});
// Music (optional, not played here)
// Sound
// Bubble shot
// Cannon
// Bubble colors
// --- Game constants ---
// --- Bubble asset variants grouped for easy management ---
// --- Shot bubble asset variants ---
// --- Other assets ---
// Background image asset (full screen)
var GAME_W = 2048;
var GAME_H = 2732;
var GRID_COLS = 10;
var GRID_ROWS = 12;
var BUBBLE_SIZE = 120; // px
var GRID_TOP = 200;
var GRID_LEFT = (GAME_W - GRID_COLS * BUBBLE_SIZE) / 2;
var GRID_BOTTOM = GRID_TOP + GRID_ROWS * BUBBLE_SIZE;
var COLORS = ['red', 'blue', 'green', 'yellow', 'purple', 'orange'];
var START_COLORS = 3; // Start with 3 colors, increase as XP increases
var XP_PER_BUBBLE = 10;
// --- Game state ---
var grid = []; // 2D array [row][col] of Bubble or null
var bubbles = []; // All Bubble instances
var shotBubbles = []; // All ShotBubble instances
var cannon;
var nextBubbleColor = null;
var canShoot = true;
var score = 0;
var xp = 0;
var level = 1;
var lastTouchX = 0,
lastTouchY = 0;
var dragAiming = false;
var gameOver = false;
// --- UI ---
var scoreTxt = new Text2('XP: 0', {
size: 100,
fill: "#fff",
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" // Use a bolder font
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var levelTxt = new Text2('Level 1', {
size: 60,
fill: "#fff",
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" // Use a bolder font
});
levelTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(levelTxt);
levelTxt.y = 110;
// --- Helper functions ---
// Get color for new bubble (based on current grid)
function getRandomColor() {
// Only use colors present in grid, or up to current level
var available = {};
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
var b = grid[r][c];
if (b && !available[b.color]) available[b.color] = true;
}
}
var colorList = [];
for (var i = 0; i < START_COLORS + level - 1 && i < COLORS.length; i++) colorList.push(COLORS[i]);
var gridColors = [];
for (var k in available) gridColors.push(k);
var useColors = gridColors.length ? gridColors : colorList;
return useColors[Math.floor(Math.random() * useColors.length)];
}
// Convert grid row,col to x,y
function gridToXY(row, col) {
var x = GRID_LEFT + col * BUBBLE_SIZE + BUBBLE_SIZE / 2;
var y = GRID_TOP + row * BUBBLE_SIZE + BUBBLE_SIZE / 2;
return {
x: x,
y: y
};
}
// Convert x,y to grid row,col
function xyToGrid(x, y) {
var col = Math.floor((x - GRID_LEFT) / BUBBLE_SIZE);
var row = Math.floor((y - GRID_TOP) / BUBBLE_SIZE);
return {
row: row,
col: col
};
}
// Check if row,col is in grid
function inGrid(row, col) {
return row >= 0 && row < GRID_ROWS && col >= 0 && col < GRID_COLS;
}
// Find all connected bubbles of same color (flood fill)
function findConnected(row, col, color, visited) {
if (!inGrid(row, col)) return [];
if (!grid[row][col]) return [];
if (grid[row][col].color !== color) return [];
var key = row + ',' + col;
if (visited[key]) return [];
visited[key] = true;
var found = [[row, col]];
// 4-way neighbors
var dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (var i = 0; i < dirs.length; i++) {
var nr = row + dirs[i][0],
nc = col + dirs[i][1];
if (inGrid(nr, nc)) {
var more = findConnected(nr, nc, color, visited);
for (var j = 0; j < more.length; j++) found.push(more[j]);
}
}
return found;
}
// Remove bubbles at positions
function popBubbles(positions) {
for (var i = 0; i < positions.length; i++) {
var rc = positions[i];
var b = grid[rc[0]][rc[1]];
if (b) {
b.pop();
grid[rc[0]][rc[1]] = null;
}
}
// Check for win condition: all bubbles cleared in level 2
if (level === 2) {
var anyBubble = false;
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
if (grid[r][c]) {
anyBubble = true;
break;
}
}
if (anyBubble) break;
}
if (!anyBubble) {
// All bubbles cleared, show you win
LK.effects.flashScreen(0x00ff00, 1000);
LK.showYouWin();
gameOver = true;
}
}
}
// Drop floating bubbles (not connected to top)
function dropFloatingBubbles() {
var connected = {};
// Mark all bubbles connected to top row
function markConnected(row, col) {
var key = row + ',' + col;
if (!inGrid(row, col)) return;
if (!grid[row][col]) return;
if (connected[key]) return;
connected[key] = true;
var dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (var i = 0; i < dirs.length; i++) {
var nr = row + dirs[i][0],
nc = col + dirs[i][1];
markConnected(nr, nc);
}
}
for (var c = 0; c < GRID_COLS; c++) {
if (grid[0][c]) markConnected(0, c);
}
// Any bubble not in connected is floating
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
var key = r + ',' + c;
if (grid[r][c] && !connected[key]) {
grid[r][c].pop();
grid[r][c] = null;
}
}
}
}
// Check for game over (bubbles at bottom row)
function checkGameOver() {
for (var c = 0; c < GRID_COLS; c++) {
if (grid[GRID_ROWS - 1][c]) {
gameOver = true;
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return true;
}
}
return false;
}
// Add XP and update UI
function addXP(amount) {
xp += amount;
scoreTxt.setText('XP: ' + xp);
// Duplicate the player (cannon) for help every time XP is gained
if (typeof cannons === "undefined") {
cannons = [cannon];
}
var newCannon = new Cannon();
newCannon.x = GAME_W / 2 + cannons.length * 120 - 60 * (cannons.length % 2 === 0 ? 1 : -1);
newCannon.y = GAME_H - 180;
newCannon.setAngle(-Math.PI / 2);
game.addChild(newCannon);
cannons.push(newCannon);
// --- You Win at 1000 XP ---
if (xp >= 1000 && !gameOver) {
gameOver = true;
// Show a custom you win overlay with a respawn button
var overlay = new Container();
overlay.x = 0;
overlay.y = 0;
overlay.width = GAME_W;
overlay.height = GAME_H;
// Dim background
var bgRect = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
alpha: 0.7
});
overlay.addChild(bgRect);
// "You Win!" text
var winTxt = new Text2('You Win!', {
size: 200,
fill: "#fff",
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" // Use a bolder font
});
winTxt.anchor.set(0.5, 0.5);
winTxt.x = GAME_W / 2;
winTxt.y = GAME_H / 2 - 200;
overlay.addChild(winTxt);
// Respawn button
var btnW = 600,
btnH = 180;
var btn = LK.getAsset('bubble_green', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_W / 2,
y: GAME_H / 2 + 100,
scaleX: btnW / 120,
scaleY: btnH / 120
});
overlay.addChild(btn);
var btnTxt = new Text2('Respawn', {
size: 100,
fill: "#222",
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" // Use a bolder font
});
btnTxt.anchor.set(0.5, 0.5);
btnTxt.x = GAME_W / 2;
btnTxt.y = GAME_H / 2 + 100;
overlay.addChild(btnTxt);
// Button interaction
btn.down = function (x, y, obj) {
// Remove overlay
if (overlay.parent) overlay.parent.removeChild(overlay);
// Restart game
startGame();
};
// Add overlay to game
game.addChild(overlay);
// Prevent further XP gain or actions
return;
}
// Level up every 200 XP
var newLevel = 1 + Math.floor(xp / 200);
if (newLevel > level) {
level = newLevel;
levelTxt.setText('Level ' + level);
if (level === 2) {
// Start level 2: reset grid with more balls
// Remove all bubbles from game
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
if (grid[r][c]) {
grid[r][c].destroy();
grid[r][c] = null;
}
}
}
// Remove all shot bubbles
for (var i = 0; i < shotBubbles.length; i++) {
if (shotBubbles[i]) shotBubbles[i].destroy();
}
shotBubbles = [];
// Reset cannons to just one
for (var i = 1; i < cannons.length; i++) {
if (cannons[i]) cannons[i].destroy();
}
cannons = [cannon];
// Re-initialize grid for level 2
initGrid();
pickNextBubbleColor();
canShoot = true;
gameOver = false;
} else {
// Add new color if possible
if (START_COLORS + level - 1 <= COLORS.length) {
// Add a row of new color bubbles at top
addNewRow();
}
}
}
}
// Add a new row of bubbles at the top (for level up)
function addNewRow() {
// Shift all rows down
for (var r = GRID_ROWS - 1; r > 0; r--) {
for (var c = 0; c < GRID_COLS; c++) {
if (grid[r - 1][c]) {
grid[r][c] = grid[r - 1][c];
grid[r][c].gridRow = r;
var pos = gridToXY(r, c);
tween(grid[r][c], {
x: pos.x,
y: pos.y
}, {
duration: 200,
easing: tween.easeOut
});
} else {
grid[r][c] = null;
}
}
}
// Add new row
var colorIdx = Math.min(START_COLORS + level - 2, COLORS.length - 1);
var color = COLORS[colorIdx];
for (var c = 0; c < GRID_COLS; c++) {
var b = new Bubble();
b.init(color);
var pos = gridToXY(0, c);
b.x = pos.x;
b.y = pos.y;
b.gridRow = 0;
b.gridCol = c;
grid[0][c] = b;
game.addChild(b);
}
}
// --- Game setup ---
// Initialize grid
function initGrid() {
grid = [];
var fillRows = 8;
if (level === 2) {
fillRows = 12; // Fill all rows for level 2
}
for (var r = 0; r < GRID_ROWS; r++) {
grid[r] = [];
for (var c = 0; c < GRID_COLS; c++) {
if (r < fillRows) {
// Fill top rows with random color
var colorIdx = Math.floor(Math.random() * (START_COLORS + level - 1));
var color = COLORS[colorIdx];
var b = new Bubble();
b.init(color);
var pos = gridToXY(r, c);
b.x = pos.x;
b.y = pos.y;
b.gridRow = r;
b.gridCol = c;
grid[r][c] = b;
game.addChild(b);
} else {
grid[r][c] = null;
}
}
}
}
// Initialize cannon
function initCannon() {
cannon = new Cannon();
cannon.x = GAME_W / 2;
cannon.y = GAME_H - 180;
cannon.setAngle(-Math.PI / 2);
game.addChild(cannon);
}
// Prepare next bubble color
function pickNextBubbleColor() {
nextBubbleColor = getRandomColor();
}
// Shoot a bubble
function shootBubble(angle) {
if (!canShoot || gameOver) return;
canShoot = false;
// If cannons array is not defined, fallback to single cannon
if (typeof cannons === "undefined" || !Array.isArray(cannons)) {
cannons = [cannon];
}
// Each cannon shoots a bubble
for (var i = 0; i < cannons.length; i++) {
var c = cannons[i];
var b = new ShotBubble();
b.init(nextBubbleColor, c.angle);
b.x = c.x;
b.y = c.y - 60;
shotBubbles.push(b);
game.addChild(b);
LK.getSound('shoot').play();
}
pickNextBubbleColor();
// Allow next shot after short delay
LK.setTimeout(function () {
canShoot = true;
}, 300);
}
// --- Input handling ---
// Aim cannon with drag/tap
function aimAt(x, y) {
// Clamp to avoid aiming below horizontal
var dx = x - cannon.x;
var dy = y - cannon.y;
var angle = Math.atan2(dy, dx);
cannon.setAngle(angle);
lastTouchX = x;
lastTouchY = y;
}
// Shoot on release
function tryShoot() {
if (!canShoot || gameOver) return;
shootBubble(cannon.angle);
}
// Touch/drag events
game.down = function (x, y, obj) {
if (gameOver) return;
if (y > GAME_H - 500) {
dragAiming = true;
aimAt(x, y);
}
};
game.move = function (x, y, obj) {
if (dragAiming && !gameOver) {
aimAt(x, y);
// --- Swipe to crush 4+ adjacent bubbles ---
// Convert to grid coordinates
var pos = xyToGrid(x, y);
var row = pos.row,
col = pos.col;
if (inGrid(row, col)) {
var gb = grid[row][col];
if (gb && !gb.popped && gb.asset && gb.asset._wasSwiped !== true) {
// Find all connected bubbles of same color
var connected = findConnected(row, col, gb.color, {});
if (connected.length >= 4) {
// Mark all as swiped to prevent double pop in one swipe
for (var i = 0; i < connected.length; i++) {
var rc = connected[i];
if (grid[rc[0]][rc[1]] && grid[rc[0]][rc[1]].asset) {
grid[rc[0]][rc[1]].asset._wasSwiped = true;
}
}
popBubbles(connected);
addXP(connected.length * XP_PER_BUBBLE);
dropFloatingBubbles();
}
}
}
}
};
game.up = function (x, y, obj) {
if (dragAiming && !gameOver) {
tryShoot();
dragAiming = false;
}
};
// --- Main update loop ---
game.update = function () {
// Update shot bubbles
for (var i = shotBubbles.length - 1; i >= 0; i--) {
var b = shotBubbles[i];
b.update();
// Out of bounds
if (b.x < 0 || b.x > GAME_W || b.y < 0) {
b.destroy();
shotBubbles.splice(i, 1);
continue;
}
// Collision with grid
var hit = false;
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
var gb = grid[r][c];
if (gb) {
var dx = b.x - gb.x;
var dy = b.y - gb.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < BUBBLE_SIZE - 10) {
// Stick bubble to grid
var pos = xyToGrid(b.x, b.y);
var row = pos.row,
col = pos.col;
if (!inGrid(row, col)) {
// Out of grid, game over
b.destroy();
shotBubbles.splice(i, 1);
checkGameOver();
return;
}
// Find nearest empty cell
if (grid[row][col]) {
// Try above
if (row > 0 && !grid[row - 1][col]) row--;else if (col > 0 && !grid[row][col - 1]) col--;else if (col < GRID_COLS - 1 && !grid[row][col + 1]) col++;
}
// Place bubble
b.stick();
var pos2 = gridToXY(row, col);
tween(b, {
x: pos2.x,
y: pos2.y
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {}
});
b.gridRow = row;
b.gridCol = col;
// Convert to grid bubble
var newB = new Bubble();
newB.init(b.color);
newB.x = pos2.x;
newB.y = pos2.y;
newB.gridRow = row;
newB.gridCol = col;
grid[row][col] = newB;
game.addChild(newB);
// Remove shot bubble
b.destroy();
shotBubbles.splice(i, 1);
// Check for matches
var connected = findConnected(row, col, newB.color, {});
if (connected.length >= 3) {
popBubbles(connected);
addXP(connected.length * XP_PER_BUBBLE);
dropFloatingBubbles();
}
checkGameOver();
hit = true;
break;
}
}
}
if (hit) break;
}
}
// --- Tap to pop grid bubbles and earn XP ---
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
var gb = grid[r][c];
if (gb && !gb.popped && gb.asset && gb.asset._wasDown !== true && gb.asset._downListenerSet !== true) {
// Set a one-time down listener for this bubble
gb.asset._downListenerSet = true;
(function (gb, r, c) {
gb.asset.down = function (x, y, obj) {
if (gb.popped) return;
gb.pop();
grid[r][c] = null;
addXP(XP_PER_BUBBLE);
};
})(gb, r, c);
}
}
}
};
// --- Game start ---
function startGame() {
xp = 0;
level = 1;
scoreTxt.setText('XP: 0');
levelTxt.setText('Level 1');
gameOver = false;
shotBubbles = [];
canShoot = true;
// Add background image to the game scene
var bg = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChildAt(bg, 0);
initGrid();
initCannon();
pickNextBubbleColor();
}
startGame();
;
LK.playMusic('Space');
Blue bubble. In-Game asset. 2d. High contrast. No shadows
Red bubble. In-Game asset. 2d. High contrast. No shadows
Orange bubble with question mark. In-Game asset. 2d. High contrast. No shadows
Purple bubble with smiley face. In-Game asset. 2d. High contrast. No shadows
Cannon. In-Game asset. 2d. High contrast. No shadows
Space. In-Game asset. 2d. High contrast. No shadows
Wood texture. In-Game asset. 2d. High contrast. No shadows