/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0,
level: 1
});
/****
* Classes
****/
var Bubble = Container.expand(function (color, size) {
var self = Container.call(this);
self.bubbleColor = color || getRandomColor();
self.bubbleSize = size || 80;
self.isMatched = false;
self.isPowerUp = false;
self.powerUpType = null;
self.row = 0;
self.col = 0;
self.falling = false;
var assetId = 'bubble_' + self.bubbleColor;
self.graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: self.bubbleSize / 80,
scaleY: self.bubbleSize / 80
});
self.convertToPowerUp = function (type) {
self.isPowerUp = true;
self.powerUpType = type;
// Replace the graphics with powerup graphics
self.removeChild(self.graphics);
var powerupAssetId = 'powerup_' + type;
self.graphics = self.attachAsset(powerupAssetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: self.bubbleSize / 80,
scaleY: self.bubbleSize / 80
});
};
self.match = function () {
if (self.isMatched) return;
self.isMatched = true;
// Play pop animation
tween(self.graphics, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
self.move = function (x, y) {
self.falling = true;
tween(self, {
x: x,
y: y
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
self.falling = false;
}
});
};
self.update = function () {
// Bubble specific update logic
if (self.falling === false && self.y > bottomLineY) {
// Bubble has crossed the bottom line
gameOver();
}
};
self.down = function (x, y, obj) {
// Handle bubble click/tap
if (game && !self.falling) {
game.checkMatches(self);
}
};
return self;
});
var BubbleBullet = Container.expand(function (color) {
var self = Container.call(this);
// Bullet properties
self.bubbleColor = color || getRandomColor();
self.bubbleSize = BUBBLE_SIZE;
self.isPowerUp = false;
self.powerUpType = null;
self.velocityX = 0;
self.velocityY = -15;
self.lastX = 0;
self.lastY = 0;
// Create graphics
var assetId = 'bubble_' + self.bubbleColor;
self.graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.convertToPowerUp = function (type) {
self.isPowerUp = true;
self.powerUpType = type;
// Replace the graphics with powerup graphics
self.removeChild(self.graphics);
var powerupAssetId = 'powerup_' + type;
self.graphics = self.attachAsset(powerupAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
};
// Update bullet position and check collisions
self.update = function () {
// Save last position
self.lastX = self.x;
self.lastY = self.y;
// Update position
self.x += self.velocityX;
self.y += self.velocityY;
// Handle wall collisions
if (self.x < GRID_LEFT_MARGIN + BUBBLE_SIZE / 2) {
self.x = GRID_LEFT_MARGIN + BUBBLE_SIZE / 2;
self.velocityX = -self.velocityX;
} else if (self.x > GRID_LEFT_MARGIN + COLS * (BUBBLE_SIZE + BUBBLE_SPACING) - BUBBLE_SIZE / 2) {
self.x = GRID_LEFT_MARGIN + COLS * (BUBBLE_SIZE + BUBBLE_SPACING) - BUBBLE_SIZE / 2;
self.velocityX = -self.velocityX;
}
// Check for collision with existing bubbles
if (self.y < GRID_TOP_MARGIN + ROWS * (BUBBLE_SIZE + BUBBLE_SPACING)) {
var collision = checkBulletCollision(self);
if (collision) {
// Attach to grid
attachBulletToGrid(self, collision);
return;
}
}
// Check if reached top
if (self.y < GRID_TOP_MARGIN) {
attachBulletToGrid(self, {
row: 0,
col: Math.floor((self.x - GRID_LEFT_MARGIN) / (BUBBLE_SIZE + BUBBLE_SPACING))
});
return;
}
};
return self;
});
var Shooter = Container.expand(function () {
var self = Container.call(this);
// Create shooter base
self.base = LK.getAsset('bubble_blue', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
self.addChild(self.base);
// Create loaded bubble
self.loadedBubble = new Bubble();
self.loadedBubble.y = -BUBBLE_SIZE - 10;
self.addChild(self.loadedBubble);
// Track next bubble to load
self.nextBubble = new Bubble();
self.nextBubble.y = -BUBBLE_SIZE - 80;
self.nextBubble.x = BUBBLE_SIZE + 10;
self.addChild(self.nextBubble);
// Shooter properties
self.angle = 0;
self.minAngle = -85;
self.maxAngle = 85;
self.canShoot = true;
// Aim the shooter
self.aim = function (targetX, targetY) {
// Calculate angle
var dx = targetX - self.x;
var dy = targetY - self.y;
var angle = Math.atan2(dx, -dy) * 180 / Math.PI;
// Limit angle
angle = Math.max(self.minAngle, Math.min(self.maxAngle, angle));
self.angle = angle;
// Rotate shooter
self.base.rotation = angle * Math.PI / 180;
};
// Shoot the loaded bubble
self.shoot = function () {
if (!self.canShoot || !gameActive) return;
// Create bullet
var bullet = new BubbleBullet(self.loadedBubble.bubbleColor);
if (self.loadedBubble.isPowerUp) {
bullet.convertToPowerUp(self.loadedBubble.powerUpType);
}
// Set initial position
bullet.x = self.x;
bullet.y = self.y - BUBBLE_SIZE;
// Set velocity based on angle
var angleRad = self.angle * Math.PI / 180;
bullet.velocityX = Math.sin(angleRad) * 20;
bullet.velocityY = -Math.cos(angleRad) * 20;
// Add to game
game.addChild(bullet);
game.bullets.push(bullet);
// Play shooting sound
LK.getSound('pop').play();
// Load next bubble
self.loadedBubble.bubbleColor = self.nextBubble.bubbleColor;
self.loadedBubble.isPowerUp = self.nextBubble.isPowerUp;
self.loadedBubble.powerUpType = self.nextBubble.powerUpType;
if (self.loadedBubble.isPowerUp) {
self.loadedBubble.convertToPowerUp(self.loadedBubble.powerUpType);
} else {
// Reset graphics to show correct color
self.loadedBubble.removeChild(self.loadedBubble.graphics);
var assetId = 'bubble_' + self.loadedBubble.bubbleColor;
self.loadedBubble.graphics = self.loadedBubble.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: self.loadedBubble.bubbleSize / 80,
scaleY: self.loadedBubble.bubbleSize / 80
});
}
// Create new next bubble
self.nextBubble.bubbleColor = getRandomColor();
self.nextBubble.isPowerUp = false;
if (Math.random() < POWERUP_CHANCE) {
var powerupType = POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)];
self.nextBubble.convertToPowerUp(powerupType);
} else {
// Reset graphics to show correct color
self.nextBubble.removeChild(self.nextBubble.graphics);
var assetId = 'bubble_' + self.nextBubble.bubbleColor;
self.nextBubble.graphics = self.nextBubble.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: self.nextBubble.bubbleSize / 80,
scaleY: self.nextBubble.bubbleSize / 80
});
}
// Disable shooting briefly
self.canShoot = false;
LK.setTimeout(function () {
self.canShoot = true;
}, 500);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game Constants
var COLS = 8;
var ROWS = 12;
var BUBBLE_SIZE = 80;
var BUBBLE_SPACING = 10;
var GRID_TOP_MARGIN = 150;
var GRID_LEFT_MARGIN = (2048 - COLS * (BUBBLE_SIZE + BUBBLE_SPACING)) / 2;
var SPAWN_INTERVAL = 2000;
var COLORS = ['red', 'blue', 'green', 'yellow', 'purple'];
var POWERUP_CHANCE = 0.05;
var POWERUP_TYPES = ['bomb', 'line'];
var bottomLineY = 2732 - 200;
// Game Variables
var bubbleGrid = [];
var gridPositions = [];
var score = 0;
var level = storage.level || 1;
var highScore = storage.highScore || 0;
var spawnTimer = null;
var levelUpThreshold = 1000;
var gameActive = true;
var comboMultiplier = 1;
var shooter = null;
game.bullets = []; // Make bullets a property of the game instance
// Game Elements
var background = LK.getAsset('game_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(background);
// Bottom line (game over line)
var bottomLine = LK.getAsset('bottom_line', {
anchorX: 0,
anchorY: 0,
x: 0,
y: bottomLineY
});
game.addChild(bottomLine);
// Setup UI
var scoreTxt = new Text2('Score: 0', {
size: 80,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0, 0);
scoreTxt.x = 50;
scoreTxt.y = 50;
LK.gui.addChild(scoreTxt);
var levelTxt = new Text2('Level: ' + level, {
size: 80,
fill: 0xFFFFFF
});
levelTxt.anchor.set(1, 0);
levelTxt.x = 2048 - 50;
levelTxt.y = 50;
LK.gui.addChild(levelTxt);
var highScoreTxt = new Text2('High Score: ' + highScore, {
size: 60,
fill: 0xFFDD00
});
highScoreTxt.anchor.set(0.5, 0);
highScoreTxt.x = 2048 / 2;
highScoreTxt.y = 50;
LK.gui.addChild(highScoreTxt);
// Helper Functions
function getRandomColor() {
return COLORS[Math.floor(Math.random() * COLORS.length)];
}
function initialSetup() {
// Calculate grid positions
calculateGridPositions();
// Initialize bubble grid
initializeGrid();
// Initialize shooter
initializeShooter();
// Play background music
LK.playMusic('bgmusic');
}
function initializeShooter() {
// Create shooter
shooter = new Shooter();
shooter.x = 2048 / 2;
shooter.y = 2732 - 200;
game.addChild(shooter);
// The bullets array is now initialized as game.bullets
}
function calculateGridPositions() {
gridPositions = [];
for (var row = 0; row < ROWS; row++) {
gridPositions[row] = [];
for (var col = 0; col < COLS; col++) {
var x = GRID_LEFT_MARGIN + col * (BUBBLE_SIZE + BUBBLE_SPACING) + BUBBLE_SIZE / 2;
var y = GRID_TOP_MARGIN + row * (BUBBLE_SIZE + BUBBLE_SPACING) + BUBBLE_SIZE / 2;
gridPositions[row][col] = {
x: x,
y: y
};
}
}
}
function initializeGrid() {
// Initialize empty grid
for (var row = 0; row < ROWS; row++) {
bubbleGrid[row] = [];
for (var col = 0; col < COLS; col++) {
bubbleGrid[row][col] = null;
}
}
// Add initial bubbles (first few rows)
for (var row = 0; row < 3; row++) {
for (var col = 0; col < COLS; col++) {
if (Math.random() < 0.4 + row * 0.1) {
// Chance increases for lower rows
spawnBubbleAt(row, col);
}
}
}
}
function spawnBubbleAt(row, col) {
if (bubbleGrid[row][col] !== null) return;
var bubble = new Bubble();
bubble.row = row;
bubble.col = col;
// Randomly convert to powerup
if (Math.random() < POWERUP_CHANCE) {
var powerupType = POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)];
bubble.convertToPowerUp(powerupType);
}
bubble.x = gridPositions[row][col].x;
bubble.y = gridPositions[row][col].y - 300; // Start above the grid
game.addChild(bubble);
// Animate to correct position
bubble.move(gridPositions[row][col].x, gridPositions[row][col].y);
bubbleGrid[row][col] = bubble;
return bubble;
}
function startSpawning() {
// Clear any existing timer
if (spawnTimer) {
LK.clearInterval(spawnTimer);
}
// Adjust spawn interval based on level
var adjustedInterval = Math.max(300, SPAWN_INTERVAL - level * 100);
spawnTimer = LK.setInterval(function () {
if (!gameActive) return;
// Shift all bubbles down by one row
shiftBubblesDown();
// Spawn new bubbles in top row
for (var col = 0; col < COLS; col++) {
if (Math.random() < 0.3 + level * 0.03) {
// Chance increases with level
spawnBubbleAt(0, col);
}
}
}, adjustedInterval);
}
function shiftBubblesDown() {
// Start from bottom row and move up
for (var row = ROWS - 1; row > 0; row--) {
for (var col = 0; col < COLS; col++) {
// Move bubble from row-1 to row
bubbleGrid[row][col] = bubbleGrid[row - 1][col];
if (bubbleGrid[row][col]) {
bubbleGrid[row][col].row = row;
bubbleGrid[row][col].move(gridPositions[row][col].x, gridPositions[row][col].y);
}
}
}
// Clear top row
for (var col = 0; col < COLS; col++) {
bubbleGrid[0][col] = null;
}
}
function checkForMatches(bubble) {
if (!bubble || bubble.isMatched || bubble.falling) return [];
var visited = {};
var matches = [];
var color = bubble.bubbleColor;
function visitBubble(r, c) {
if (r < 0 || c < 0 || r >= ROWS || c >= COLS) return;
if (visited[r + "," + c]) return;
if (!bubbleGrid[r][c]) return;
if (bubbleGrid[r][c].isMatched) return;
if (bubbleGrid[r][c].falling) return;
if (!bubble.isPowerUp && bubbleGrid[r][c].bubbleColor !== color) return;
visited[r + "," + c] = true;
matches.push(bubbleGrid[r][c]);
// Check neighbors
visitBubble(r - 1, c); // up
visitBubble(r + 1, c); // down
visitBubble(r, c - 1); // left
visitBubble(r, c + 1); // right
}
visitBubble(bubble.row, bubble.col);
return matches;
}
function popBubbles(bubbles) {
if (!bubbles || bubbles.length === 0) return;
// Apply powerups
for (var i = 0; i < bubbles.length; i++) {
var bubble = bubbles[i];
if (bubble.isPowerUp) {
if (bubble.powerUpType === 'bomb') {
// Bomb: pop all adjacent bubbles
for (var r = Math.max(0, bubble.row - 1); r <= Math.min(ROWS - 1, bubble.row + 1); r++) {
for (var c = Math.max(0, bubble.col - 1); c <= Math.min(COLS - 1, bubble.col + 1); c++) {
if (bubbleGrid[r][c] && !bubbleGrid[r][c].isMatched) {
bubbles.push(bubbleGrid[r][c]);
}
}
}
LK.getSound('powerup').play();
} else if (bubble.powerUpType === 'line') {
// Line: pop entire row
for (var c = 0; c < COLS; c++) {
if (bubbleGrid[bubble.row][c] && !bubbleGrid[bubble.row][c].isMatched) {
bubbles.push(bubbleGrid[bubble.row][c]);
}
}
LK.getSound('powerup').play();
}
}
}
// Remove duplicates from the array
var uniqueBubbles = [];
var bubbleIds = {};
for (var i = 0; i < bubbles.length; i++) {
var bubbleId = bubbles[i].row + "," + bubbles[i].col;
if (!bubbleIds[bubbleId]) {
bubbleIds[bubbleId] = true;
uniqueBubbles.push(bubbles[i]);
}
}
// Calculate points
var points = uniqueBubbles.length * 10 * comboMultiplier;
// Play sound
if (uniqueBubbles.length >= 5) {
LK.getSound('combo').play();
// Increase combo multiplier
comboMultiplier++;
} else {
LK.getSound('pop').play();
}
// Pop each bubble
for (var i = 0; i < uniqueBubbles.length; i++) {
var bubble = uniqueBubbles[i];
// Clear from grid
bubbleGrid[bubble.row][bubble.col] = null;
// Animate and destroy
bubble.match();
}
// Update score
updateScore(points);
// Reset combo after a short delay
LK.setTimeout(function () {
comboMultiplier = 1;
}, 1500);
}
function updateScore(points) {
score += points;
scoreTxt.setText('Score: ' + score);
// Check for level up
if (score >= level * levelUpThreshold) {
levelUp();
}
// Update high score if needed
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
highScoreTxt.setText('High Score: ' + highScore);
}
}
function levelUp() {
level++;
storage.level = level;
levelTxt.setText('Level: ' + level);
// Flash level text
tween(levelTxt, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(levelTxt, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeIn
});
}
});
// Restart spawning with adjusted timing
startSpawning();
}
function checkBulletCollision(bullet) {
// Check collision with existing bubbles
for (var row = 0; row < ROWS; row++) {
for (var col = 0; col < COLS; col++) {
if (bubbleGrid[row][col]) {
var dx = bullet.x - gridPositions[row][col].x;
var dy = bullet.y - gridPositions[row][col].y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < BUBBLE_SIZE) {
// Find grid position to attach to
return findAttachPosition(bullet, row, col);
}
}
}
}
return null;
}
function findAttachPosition(bullet, hitRow, hitCol) {
// Potential positions
var positions = [{
row: hitRow - 1,
col: hitCol
},
// top
{
row: hitRow + 1,
col: hitCol
},
// bottom
{
row: hitRow,
col: hitCol - 1
},
// left
{
row: hitRow,
col: hitCol + 1
},
// right
{
row: hitRow - 1,
col: hitCol - 1
},
// top-left
{
row: hitRow - 1,
col: hitCol + 1
},
// top-right
{
row: hitRow + 1,
col: hitCol - 1
},
// bottom-left
{
row: hitRow + 1,
col: hitCol + 1
} // bottom-right
];
// Find closest empty position
var closestPos = null;
var closestDist = Infinity;
for (var i = 0; i < positions.length; i++) {
var pos = positions[i];
if (pos.row >= 0 && pos.row < ROWS && pos.col >= 0 && pos.col < COLS) {
if (!bubbleGrid[pos.row][pos.col]) {
var dx = bullet.x - gridPositions[pos.row][pos.col].x;
var dy = bullet.y - gridPositions[pos.row][pos.col].y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < closestDist) {
closestDist = dist;
closestPos = pos;
}
}
}
}
return closestPos;
}
function attachBulletToGrid(bullet, position) {
// Remove from bullets array
var index = game.bullets.indexOf(bullet);
if (index !== -1) {
game.bullets.splice(index, 1);
}
// Create new grid bubble
var newBubble = new Bubble(bullet.bubbleColor);
if (bullet.isPowerUp) {
newBubble.convertToPowerUp(bullet.powerUpType);
}
// Set position
newBubble.row = position.row;
newBubble.col = position.col;
newBubble.x = gridPositions[position.row][position.col].x;
newBubble.y = gridPositions[position.row][position.col].y;
// Add to grid
bubbleGrid[position.row][position.col] = newBubble;
game.addChild(newBubble);
// Play pop sound
LK.getSound('pop').play();
// Check for matches
var matches = checkForMatches(newBubble);
if (matches.length >= 3 || matches.length > 0 && newBubble.isPowerUp) {
popBubbles(matches);
}
// Remove bullet
bullet.destroy();
}
function gameOver() {
gameActive = false;
// Stop spawning
if (spawnTimer) {
LK.clearInterval(spawnTimer);
spawnTimer = null;
}
// Play game over sound
LK.getSound('gameover').play();
// Show game over screen
LK.showGameOver();
}
// Game Logic
game.checkMatches = function (clickedBubble) {
if (!gameActive) return;
var matches = checkForMatches(clickedBubble);
// Need at least 3 matching bubbles or a power-up
if (matches.length >= 3 || matches.length > 0 && clickedBubble.isPowerUp) {
popBubbles(matches);
}
};
// Game update function
game.update = function () {
if (!gameActive) return;
// Update all bubbles
for (var row = 0; row < ROWS; row++) {
for (var col = 0; col < COLS; col++) {
if (bubbleGrid[row][col]) {
bubbleGrid[row][col].update();
}
}
}
// Update all bullets
for (var i = game.bullets.length - 1; i >= 0; i--) {
game.bullets[i].update();
}
// Check for game over condition
if (bubbleGrid[ROWS - 1].some(function (bubble) {
return bubble !== null;
})) {
gameOver();
}
};
// Input handlers
game.down = function (x, y, obj) {
if (!gameActive || !shooter) return;
shooter.aim(x, y);
};
game.move = function (x, y, obj) {
if (!gameActive || !shooter) return;
shooter.aim(x, y);
};
game.up = function (x, y, obj) {
if (!gameActive || !shooter) return;
shooter.shoot();
};
// Remove startSpawning from initialSetup
// Start the game
initialSetup();
; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0,
level: 1
});
/****
* Classes
****/
var Bubble = Container.expand(function (color, size) {
var self = Container.call(this);
self.bubbleColor = color || getRandomColor();
self.bubbleSize = size || 80;
self.isMatched = false;
self.isPowerUp = false;
self.powerUpType = null;
self.row = 0;
self.col = 0;
self.falling = false;
var assetId = 'bubble_' + self.bubbleColor;
self.graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: self.bubbleSize / 80,
scaleY: self.bubbleSize / 80
});
self.convertToPowerUp = function (type) {
self.isPowerUp = true;
self.powerUpType = type;
// Replace the graphics with powerup graphics
self.removeChild(self.graphics);
var powerupAssetId = 'powerup_' + type;
self.graphics = self.attachAsset(powerupAssetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: self.bubbleSize / 80,
scaleY: self.bubbleSize / 80
});
};
self.match = function () {
if (self.isMatched) return;
self.isMatched = true;
// Play pop animation
tween(self.graphics, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
self.move = function (x, y) {
self.falling = true;
tween(self, {
x: x,
y: y
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
self.falling = false;
}
});
};
self.update = function () {
// Bubble specific update logic
if (self.falling === false && self.y > bottomLineY) {
// Bubble has crossed the bottom line
gameOver();
}
};
self.down = function (x, y, obj) {
// Handle bubble click/tap
if (game && !self.falling) {
game.checkMatches(self);
}
};
return self;
});
var BubbleBullet = Container.expand(function (color) {
var self = Container.call(this);
// Bullet properties
self.bubbleColor = color || getRandomColor();
self.bubbleSize = BUBBLE_SIZE;
self.isPowerUp = false;
self.powerUpType = null;
self.velocityX = 0;
self.velocityY = -15;
self.lastX = 0;
self.lastY = 0;
// Create graphics
var assetId = 'bubble_' + self.bubbleColor;
self.graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.convertToPowerUp = function (type) {
self.isPowerUp = true;
self.powerUpType = type;
// Replace the graphics with powerup graphics
self.removeChild(self.graphics);
var powerupAssetId = 'powerup_' + type;
self.graphics = self.attachAsset(powerupAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
};
// Update bullet position and check collisions
self.update = function () {
// Save last position
self.lastX = self.x;
self.lastY = self.y;
// Update position
self.x += self.velocityX;
self.y += self.velocityY;
// Handle wall collisions
if (self.x < GRID_LEFT_MARGIN + BUBBLE_SIZE / 2) {
self.x = GRID_LEFT_MARGIN + BUBBLE_SIZE / 2;
self.velocityX = -self.velocityX;
} else if (self.x > GRID_LEFT_MARGIN + COLS * (BUBBLE_SIZE + BUBBLE_SPACING) - BUBBLE_SIZE / 2) {
self.x = GRID_LEFT_MARGIN + COLS * (BUBBLE_SIZE + BUBBLE_SPACING) - BUBBLE_SIZE / 2;
self.velocityX = -self.velocityX;
}
// Check for collision with existing bubbles
if (self.y < GRID_TOP_MARGIN + ROWS * (BUBBLE_SIZE + BUBBLE_SPACING)) {
var collision = checkBulletCollision(self);
if (collision) {
// Attach to grid
attachBulletToGrid(self, collision);
return;
}
}
// Check if reached top
if (self.y < GRID_TOP_MARGIN) {
attachBulletToGrid(self, {
row: 0,
col: Math.floor((self.x - GRID_LEFT_MARGIN) / (BUBBLE_SIZE + BUBBLE_SPACING))
});
return;
}
};
return self;
});
var Shooter = Container.expand(function () {
var self = Container.call(this);
// Create shooter base
self.base = LK.getAsset('bubble_blue', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
self.addChild(self.base);
// Create loaded bubble
self.loadedBubble = new Bubble();
self.loadedBubble.y = -BUBBLE_SIZE - 10;
self.addChild(self.loadedBubble);
// Track next bubble to load
self.nextBubble = new Bubble();
self.nextBubble.y = -BUBBLE_SIZE - 80;
self.nextBubble.x = BUBBLE_SIZE + 10;
self.addChild(self.nextBubble);
// Shooter properties
self.angle = 0;
self.minAngle = -85;
self.maxAngle = 85;
self.canShoot = true;
// Aim the shooter
self.aim = function (targetX, targetY) {
// Calculate angle
var dx = targetX - self.x;
var dy = targetY - self.y;
var angle = Math.atan2(dx, -dy) * 180 / Math.PI;
// Limit angle
angle = Math.max(self.minAngle, Math.min(self.maxAngle, angle));
self.angle = angle;
// Rotate shooter
self.base.rotation = angle * Math.PI / 180;
};
// Shoot the loaded bubble
self.shoot = function () {
if (!self.canShoot || !gameActive) return;
// Create bullet
var bullet = new BubbleBullet(self.loadedBubble.bubbleColor);
if (self.loadedBubble.isPowerUp) {
bullet.convertToPowerUp(self.loadedBubble.powerUpType);
}
// Set initial position
bullet.x = self.x;
bullet.y = self.y - BUBBLE_SIZE;
// Set velocity based on angle
var angleRad = self.angle * Math.PI / 180;
bullet.velocityX = Math.sin(angleRad) * 20;
bullet.velocityY = -Math.cos(angleRad) * 20;
// Add to game
game.addChild(bullet);
game.bullets.push(bullet);
// Play shooting sound
LK.getSound('pop').play();
// Load next bubble
self.loadedBubble.bubbleColor = self.nextBubble.bubbleColor;
self.loadedBubble.isPowerUp = self.nextBubble.isPowerUp;
self.loadedBubble.powerUpType = self.nextBubble.powerUpType;
if (self.loadedBubble.isPowerUp) {
self.loadedBubble.convertToPowerUp(self.loadedBubble.powerUpType);
} else {
// Reset graphics to show correct color
self.loadedBubble.removeChild(self.loadedBubble.graphics);
var assetId = 'bubble_' + self.loadedBubble.bubbleColor;
self.loadedBubble.graphics = self.loadedBubble.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: self.loadedBubble.bubbleSize / 80,
scaleY: self.loadedBubble.bubbleSize / 80
});
}
// Create new next bubble
self.nextBubble.bubbleColor = getRandomColor();
self.nextBubble.isPowerUp = false;
if (Math.random() < POWERUP_CHANCE) {
var powerupType = POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)];
self.nextBubble.convertToPowerUp(powerupType);
} else {
// Reset graphics to show correct color
self.nextBubble.removeChild(self.nextBubble.graphics);
var assetId = 'bubble_' + self.nextBubble.bubbleColor;
self.nextBubble.graphics = self.nextBubble.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: self.nextBubble.bubbleSize / 80,
scaleY: self.nextBubble.bubbleSize / 80
});
}
// Disable shooting briefly
self.canShoot = false;
LK.setTimeout(function () {
self.canShoot = true;
}, 500);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game Constants
var COLS = 8;
var ROWS = 12;
var BUBBLE_SIZE = 80;
var BUBBLE_SPACING = 10;
var GRID_TOP_MARGIN = 150;
var GRID_LEFT_MARGIN = (2048 - COLS * (BUBBLE_SIZE + BUBBLE_SPACING)) / 2;
var SPAWN_INTERVAL = 2000;
var COLORS = ['red', 'blue', 'green', 'yellow', 'purple'];
var POWERUP_CHANCE = 0.05;
var POWERUP_TYPES = ['bomb', 'line'];
var bottomLineY = 2732 - 200;
// Game Variables
var bubbleGrid = [];
var gridPositions = [];
var score = 0;
var level = storage.level || 1;
var highScore = storage.highScore || 0;
var spawnTimer = null;
var levelUpThreshold = 1000;
var gameActive = true;
var comboMultiplier = 1;
var shooter = null;
game.bullets = []; // Make bullets a property of the game instance
// Game Elements
var background = LK.getAsset('game_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(background);
// Bottom line (game over line)
var bottomLine = LK.getAsset('bottom_line', {
anchorX: 0,
anchorY: 0,
x: 0,
y: bottomLineY
});
game.addChild(bottomLine);
// Setup UI
var scoreTxt = new Text2('Score: 0', {
size: 80,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0, 0);
scoreTxt.x = 50;
scoreTxt.y = 50;
LK.gui.addChild(scoreTxt);
var levelTxt = new Text2('Level: ' + level, {
size: 80,
fill: 0xFFFFFF
});
levelTxt.anchor.set(1, 0);
levelTxt.x = 2048 - 50;
levelTxt.y = 50;
LK.gui.addChild(levelTxt);
var highScoreTxt = new Text2('High Score: ' + highScore, {
size: 60,
fill: 0xFFDD00
});
highScoreTxt.anchor.set(0.5, 0);
highScoreTxt.x = 2048 / 2;
highScoreTxt.y = 50;
LK.gui.addChild(highScoreTxt);
// Helper Functions
function getRandomColor() {
return COLORS[Math.floor(Math.random() * COLORS.length)];
}
function initialSetup() {
// Calculate grid positions
calculateGridPositions();
// Initialize bubble grid
initializeGrid();
// Initialize shooter
initializeShooter();
// Play background music
LK.playMusic('bgmusic');
}
function initializeShooter() {
// Create shooter
shooter = new Shooter();
shooter.x = 2048 / 2;
shooter.y = 2732 - 200;
game.addChild(shooter);
// The bullets array is now initialized as game.bullets
}
function calculateGridPositions() {
gridPositions = [];
for (var row = 0; row < ROWS; row++) {
gridPositions[row] = [];
for (var col = 0; col < COLS; col++) {
var x = GRID_LEFT_MARGIN + col * (BUBBLE_SIZE + BUBBLE_SPACING) + BUBBLE_SIZE / 2;
var y = GRID_TOP_MARGIN + row * (BUBBLE_SIZE + BUBBLE_SPACING) + BUBBLE_SIZE / 2;
gridPositions[row][col] = {
x: x,
y: y
};
}
}
}
function initializeGrid() {
// Initialize empty grid
for (var row = 0; row < ROWS; row++) {
bubbleGrid[row] = [];
for (var col = 0; col < COLS; col++) {
bubbleGrid[row][col] = null;
}
}
// Add initial bubbles (first few rows)
for (var row = 0; row < 3; row++) {
for (var col = 0; col < COLS; col++) {
if (Math.random() < 0.4 + row * 0.1) {
// Chance increases for lower rows
spawnBubbleAt(row, col);
}
}
}
}
function spawnBubbleAt(row, col) {
if (bubbleGrid[row][col] !== null) return;
var bubble = new Bubble();
bubble.row = row;
bubble.col = col;
// Randomly convert to powerup
if (Math.random() < POWERUP_CHANCE) {
var powerupType = POWERUP_TYPES[Math.floor(Math.random() * POWERUP_TYPES.length)];
bubble.convertToPowerUp(powerupType);
}
bubble.x = gridPositions[row][col].x;
bubble.y = gridPositions[row][col].y - 300; // Start above the grid
game.addChild(bubble);
// Animate to correct position
bubble.move(gridPositions[row][col].x, gridPositions[row][col].y);
bubbleGrid[row][col] = bubble;
return bubble;
}
function startSpawning() {
// Clear any existing timer
if (spawnTimer) {
LK.clearInterval(spawnTimer);
}
// Adjust spawn interval based on level
var adjustedInterval = Math.max(300, SPAWN_INTERVAL - level * 100);
spawnTimer = LK.setInterval(function () {
if (!gameActive) return;
// Shift all bubbles down by one row
shiftBubblesDown();
// Spawn new bubbles in top row
for (var col = 0; col < COLS; col++) {
if (Math.random() < 0.3 + level * 0.03) {
// Chance increases with level
spawnBubbleAt(0, col);
}
}
}, adjustedInterval);
}
function shiftBubblesDown() {
// Start from bottom row and move up
for (var row = ROWS - 1; row > 0; row--) {
for (var col = 0; col < COLS; col++) {
// Move bubble from row-1 to row
bubbleGrid[row][col] = bubbleGrid[row - 1][col];
if (bubbleGrid[row][col]) {
bubbleGrid[row][col].row = row;
bubbleGrid[row][col].move(gridPositions[row][col].x, gridPositions[row][col].y);
}
}
}
// Clear top row
for (var col = 0; col < COLS; col++) {
bubbleGrid[0][col] = null;
}
}
function checkForMatches(bubble) {
if (!bubble || bubble.isMatched || bubble.falling) return [];
var visited = {};
var matches = [];
var color = bubble.bubbleColor;
function visitBubble(r, c) {
if (r < 0 || c < 0 || r >= ROWS || c >= COLS) return;
if (visited[r + "," + c]) return;
if (!bubbleGrid[r][c]) return;
if (bubbleGrid[r][c].isMatched) return;
if (bubbleGrid[r][c].falling) return;
if (!bubble.isPowerUp && bubbleGrid[r][c].bubbleColor !== color) return;
visited[r + "," + c] = true;
matches.push(bubbleGrid[r][c]);
// Check neighbors
visitBubble(r - 1, c); // up
visitBubble(r + 1, c); // down
visitBubble(r, c - 1); // left
visitBubble(r, c + 1); // right
}
visitBubble(bubble.row, bubble.col);
return matches;
}
function popBubbles(bubbles) {
if (!bubbles || bubbles.length === 0) return;
// Apply powerups
for (var i = 0; i < bubbles.length; i++) {
var bubble = bubbles[i];
if (bubble.isPowerUp) {
if (bubble.powerUpType === 'bomb') {
// Bomb: pop all adjacent bubbles
for (var r = Math.max(0, bubble.row - 1); r <= Math.min(ROWS - 1, bubble.row + 1); r++) {
for (var c = Math.max(0, bubble.col - 1); c <= Math.min(COLS - 1, bubble.col + 1); c++) {
if (bubbleGrid[r][c] && !bubbleGrid[r][c].isMatched) {
bubbles.push(bubbleGrid[r][c]);
}
}
}
LK.getSound('powerup').play();
} else if (bubble.powerUpType === 'line') {
// Line: pop entire row
for (var c = 0; c < COLS; c++) {
if (bubbleGrid[bubble.row][c] && !bubbleGrid[bubble.row][c].isMatched) {
bubbles.push(bubbleGrid[bubble.row][c]);
}
}
LK.getSound('powerup').play();
}
}
}
// Remove duplicates from the array
var uniqueBubbles = [];
var bubbleIds = {};
for (var i = 0; i < bubbles.length; i++) {
var bubbleId = bubbles[i].row + "," + bubbles[i].col;
if (!bubbleIds[bubbleId]) {
bubbleIds[bubbleId] = true;
uniqueBubbles.push(bubbles[i]);
}
}
// Calculate points
var points = uniqueBubbles.length * 10 * comboMultiplier;
// Play sound
if (uniqueBubbles.length >= 5) {
LK.getSound('combo').play();
// Increase combo multiplier
comboMultiplier++;
} else {
LK.getSound('pop').play();
}
// Pop each bubble
for (var i = 0; i < uniqueBubbles.length; i++) {
var bubble = uniqueBubbles[i];
// Clear from grid
bubbleGrid[bubble.row][bubble.col] = null;
// Animate and destroy
bubble.match();
}
// Update score
updateScore(points);
// Reset combo after a short delay
LK.setTimeout(function () {
comboMultiplier = 1;
}, 1500);
}
function updateScore(points) {
score += points;
scoreTxt.setText('Score: ' + score);
// Check for level up
if (score >= level * levelUpThreshold) {
levelUp();
}
// Update high score if needed
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
highScoreTxt.setText('High Score: ' + highScore);
}
}
function levelUp() {
level++;
storage.level = level;
levelTxt.setText('Level: ' + level);
// Flash level text
tween(levelTxt, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(levelTxt, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeIn
});
}
});
// Restart spawning with adjusted timing
startSpawning();
}
function checkBulletCollision(bullet) {
// Check collision with existing bubbles
for (var row = 0; row < ROWS; row++) {
for (var col = 0; col < COLS; col++) {
if (bubbleGrid[row][col]) {
var dx = bullet.x - gridPositions[row][col].x;
var dy = bullet.y - gridPositions[row][col].y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < BUBBLE_SIZE) {
// Find grid position to attach to
return findAttachPosition(bullet, row, col);
}
}
}
}
return null;
}
function findAttachPosition(bullet, hitRow, hitCol) {
// Potential positions
var positions = [{
row: hitRow - 1,
col: hitCol
},
// top
{
row: hitRow + 1,
col: hitCol
},
// bottom
{
row: hitRow,
col: hitCol - 1
},
// left
{
row: hitRow,
col: hitCol + 1
},
// right
{
row: hitRow - 1,
col: hitCol - 1
},
// top-left
{
row: hitRow - 1,
col: hitCol + 1
},
// top-right
{
row: hitRow + 1,
col: hitCol - 1
},
// bottom-left
{
row: hitRow + 1,
col: hitCol + 1
} // bottom-right
];
// Find closest empty position
var closestPos = null;
var closestDist = Infinity;
for (var i = 0; i < positions.length; i++) {
var pos = positions[i];
if (pos.row >= 0 && pos.row < ROWS && pos.col >= 0 && pos.col < COLS) {
if (!bubbleGrid[pos.row][pos.col]) {
var dx = bullet.x - gridPositions[pos.row][pos.col].x;
var dy = bullet.y - gridPositions[pos.row][pos.col].y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < closestDist) {
closestDist = dist;
closestPos = pos;
}
}
}
}
return closestPos;
}
function attachBulletToGrid(bullet, position) {
// Remove from bullets array
var index = game.bullets.indexOf(bullet);
if (index !== -1) {
game.bullets.splice(index, 1);
}
// Create new grid bubble
var newBubble = new Bubble(bullet.bubbleColor);
if (bullet.isPowerUp) {
newBubble.convertToPowerUp(bullet.powerUpType);
}
// Set position
newBubble.row = position.row;
newBubble.col = position.col;
newBubble.x = gridPositions[position.row][position.col].x;
newBubble.y = gridPositions[position.row][position.col].y;
// Add to grid
bubbleGrid[position.row][position.col] = newBubble;
game.addChild(newBubble);
// Play pop sound
LK.getSound('pop').play();
// Check for matches
var matches = checkForMatches(newBubble);
if (matches.length >= 3 || matches.length > 0 && newBubble.isPowerUp) {
popBubbles(matches);
}
// Remove bullet
bullet.destroy();
}
function gameOver() {
gameActive = false;
// Stop spawning
if (spawnTimer) {
LK.clearInterval(spawnTimer);
spawnTimer = null;
}
// Play game over sound
LK.getSound('gameover').play();
// Show game over screen
LK.showGameOver();
}
// Game Logic
game.checkMatches = function (clickedBubble) {
if (!gameActive) return;
var matches = checkForMatches(clickedBubble);
// Need at least 3 matching bubbles or a power-up
if (matches.length >= 3 || matches.length > 0 && clickedBubble.isPowerUp) {
popBubbles(matches);
}
};
// Game update function
game.update = function () {
if (!gameActive) return;
// Update all bubbles
for (var row = 0; row < ROWS; row++) {
for (var col = 0; col < COLS; col++) {
if (bubbleGrid[row][col]) {
bubbleGrid[row][col].update();
}
}
}
// Update all bullets
for (var i = game.bullets.length - 1; i >= 0; i--) {
game.bullets[i].update();
}
// Check for game over condition
if (bubbleGrid[ROWS - 1].some(function (bubble) {
return bubble !== null;
})) {
gameOver();
}
};
// Input handlers
game.down = function (x, y, obj) {
if (!gameActive || !shooter) return;
shooter.aim(x, y);
};
game.move = function (x, y, obj) {
if (!gameActive || !shooter) return;
shooter.aim(x, y);
};
game.up = function (x, y, obj) {
if (!gameActive || !shooter) return;
shooter.shoot();
};
// Remove startSpawning from initialSetup
// Start the game
initialSetup();
;