User prompt
Add a more bolder font
User prompt
Make an asset for the respawn button
User prompt
When the player reaches 1000 points imploment a you win screen that has a button in the middle that respawns you back to the start
User prompt
Make a level 2 with more balls and then at the end imploment a you win screen
User prompt
Make the duplicated cannons shoot
User prompt
Everytime when the player gets a xp the player duplicates for help
User prompt
Make a background asset
User prompt
Add all bubble variabts in one area
User prompt
When game loads play music asset
User prompt
Make more bubbles
User prompt
Make it possible to crush 4 in one try with swipe
User prompt
We you tap the bubble disappears and you earn xp
Code edit (1 edits merged)
Please save this source code
User prompt
Bubble XP Shooter
Initial prompt
Bubble shooter where you shoot bubbles to earn XP
/**** * 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 --- 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" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var levelTxt = new Text2('Level 1', { size: 60, fill: "#fff" }); 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; } } } // 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); // Level up every 200 XP var newLevel = 1 + Math.floor(xp / 200); if (newLevel > level) { level = newLevel; levelTxt.setText('Level ' + level); // 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 = []; for (var r = 0; r < GRID_ROWS; r++) { grid[r] = []; for (var c = 0; c < GRID_COLS; c++) { if (r < 5) { // Fill top 5 rows with random color var colorIdx = Math.floor(Math.random() * START_COLORS); 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; var b = new ShotBubble(); b.init(nextBubbleColor, angle); b.x = cannon.x; b.y = cannon.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); } }; 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; } } }; // --- Game start --- function startGame() { xp = 0; level = 1; scoreTxt.setText('XP: 0'); levelTxt.setText('Level 1'); gameOver = false; shotBubbles = []; canShoot = true; initGrid(); initCannon(); pickNextBubbleColor(); } startGame();
===================================================================
--- original.js
+++ change.js
@@ -1,6 +1,500 @@
-/****
+/****
+* 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: 0x000000
-});
\ No newline at end of file
+ backgroundColor: 0x222a38
+});
+
+/****
+* Game Code
+****/
+// LK.init.music('bgmusic', {volume: 1});
+// Music (optional, not played here)
+// Sound
+// Bubble shot
+// Cannon
+// Bubble colors
+// --- Game constants ---
+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"
+});
+scoreTxt.anchor.set(0.5, 0);
+LK.gui.top.addChild(scoreTxt);
+var levelTxt = new Text2('Level 1', {
+ size: 60,
+ fill: "#fff"
+});
+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;
+ }
+ }
+}
+// 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);
+ // Level up every 200 XP
+ var newLevel = 1 + Math.floor(xp / 200);
+ if (newLevel > level) {
+ level = newLevel;
+ levelTxt.setText('Level ' + level);
+ // 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 = [];
+ for (var r = 0; r < GRID_ROWS; r++) {
+ grid[r] = [];
+ for (var c = 0; c < GRID_COLS; c++) {
+ if (r < 5) {
+ // Fill top 5 rows with random color
+ var colorIdx = Math.floor(Math.random() * START_COLORS);
+ 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;
+ var b = new ShotBubble();
+ b.init(nextBubbleColor, angle);
+ b.x = cannon.x;
+ b.y = cannon.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);
+ }
+};
+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;
+ }
+ }
+};
+// --- Game start ---
+function startGame() {
+ xp = 0;
+ level = 1;
+ scoreTxt.setText('XP: 0');
+ levelTxt.setText('Level 1');
+ gameOver = false;
+ shotBubbles = [];
+ canShoot = true;
+ initGrid();
+ initCannon();
+ pickNextBubbleColor();
+}
+startGame();
\ No newline at end of file
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