/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Candy = Container.expand(function (type) {
var self = Container.call(this);
self.candyType = type;
self.gridX = 0;
self.gridY = 0;
self.isAnimating = false;
self.isMatched = false;
var candyAssets = ['candy_red', 'candy_blue', 'candy_green', 'candy_yellow', 'candy_purple', 'candy_orange', 'bomb'];
var candyGraphics = self.attachAsset(candyAssets[type], {
anchorX: 0.5,
anchorY: 0.5
});
self.setGridPosition = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = GRID_START_X + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = GRID_START_Y + gridY * CELL_SIZE + CELL_SIZE / 2;
};
self.animateToPosition = function (targetX, targetY, duration, onComplete) {
self.isAnimating = true;
tween(self, {
x: targetX,
y: targetY
}, {
duration: duration || 300,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isAnimating = false;
if (onComplete) {
onComplete();
}
}
});
};
self.animateMatch = function (onComplete) {
self.isMatched = true;
tween(self, {
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: onComplete
});
};
self.explodeBomb = function (onComplete) {
self.isMatched = true;
// Scale up and fade out for bomb explosion
tween(self, {
scaleX: 3,
scaleY: 3,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: onComplete
});
};
self.isBomb = function () {
return self.candyType === 6; // Bomb is type 6
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1A1A2E
});
/****
* Game Code
****/
// Game constants
var GRID_SIZE = 8;
var CELL_SIZE = 180;
var GRID_START_X = (2048 - GRID_SIZE * CELL_SIZE) / 2;
var GRID_START_Y = 500;
var CANDY_TYPES = 7; // 6 regular candies + 1 bomb type
// Game variables
var grid = [];
var selectedCandy = null;
var isProcessing = false;
var score = 0;
var moves = 30;
var targetScore = 5000;
var draggedCandy = null;
var dragStartX = 0;
var dragStartY = 0;
var originalCandyX = 0;
var originalCandyY = 0;
// Initialize UI
var scoreText = new Text2('Score: 0', {
size: 90,
fill: '#FFFFFF'
});
scoreText.anchor.set(0.5, 0);
scoreText.y = 50;
LK.gui.top.addChild(scoreText);
var movesText = new Text2('Moves: 30', {
size: 70,
fill: '#FFD700'
});
movesText.anchor.set(0.5, 0);
movesText.y = 150;
LK.gui.top.addChild(movesText);
var targetText = new Text2('Target: 5000', {
size: 70,
fill: '#FF6B6B'
});
targetText.anchor.set(0.5, 0);
targetText.y = 220;
LK.gui.top.addChild(targetText);
// Initialize grid
function initializeGrid() {
grid = [];
for (var x = 0; x < GRID_SIZE; x++) {
grid[x] = [];
for (var y = 0; y < GRID_SIZE; y++) {
// Only spawn one bomb in the entire grid occasionally
var candyType;
var hasBombInGrid = false;
// Check if there's already a bomb in the grid
for (var checkX = 0; checkX < GRID_SIZE; checkX++) {
for (var checkY = 0; checkY < GRID_SIZE; checkY++) {
if (grid[checkX] && grid[checkX][checkY] && grid[checkX][checkY].candyType === 6) {
hasBombInGrid = true;
break;
}
}
if (hasBombInGrid) break;
}
// Only 2% chance for bomb if no bomb exists yet
if (!hasBombInGrid && Math.random() < 0.02) {
candyType = 6; // Bomb type
} else {
candyType = Math.floor(Math.random() * 6); // Regular candy types only
}
var candy = new Candy(candyType);
candy.setGridPosition(x, y);
grid[x][y] = candy;
game.addChild(candy);
}
}
// Remove initial matches
removeInitialMatches();
}
function removeInitialMatches() {
var hasMatches = true;
while (hasMatches) {
var matches = findAllMatches();
if (matches.length === 0) {
hasMatches = false;
} else {
for (var i = 0; i < matches.length; i++) {
var match = matches[i];
var newType = Math.floor(Math.random() * CANDY_TYPES);
match.candyType = newType;
match.removeChild(match.children[0]);
var candyAssets = ['candy_red', 'candy_blue', 'candy_green', 'candy_yellow', 'candy_purple', 'candy_orange', 'bomb'];
match.attachAsset(candyAssets[newType], {
anchorX: 0.5,
anchorY: 0.5
});
}
}
}
}
function getCandyAt(x, y) {
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) {
return null;
}
return grid[x][y];
}
function getCandiesAroundBomb(bombX, bombY) {
var surroundingCandies = [];
// Get all 8 surrounding cells plus the bomb itself
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
var candy = getCandyAt(bombX + dx, bombY + dy);
if (candy && !candy.isMatched) {
surroundingCandies.push(candy);
}
}
}
return surroundingCandies;
}
function swapCandies(candy1, candy2) {
if (!candy1 || !candy2 || candy1.isAnimating || candy2.isAnimating) {
return false;
}
var tempX = candy1.gridX;
var tempY = candy1.gridY;
// Update grid positions
grid[candy1.gridX][candy1.gridY] = candy2;
grid[candy2.gridX][candy2.gridY] = candy1;
// Update candy grid positions
candy1.setGridPosition(candy2.gridX, candy2.gridY);
candy2.setGridPosition(tempX, tempY);
return true;
}
function findMatches(startX, startY) {
var candy = getCandyAt(startX, startY);
if (!candy) {
return [];
}
var matches = [];
var type = candy.candyType;
// Check horizontal matches
var horizontalMatches = [candy];
// Check left
for (var x = startX - 1; x >= 0; x--) {
var leftCandy = getCandyAt(x, startY);
if (leftCandy && leftCandy.candyType === type) {
horizontalMatches.unshift(leftCandy);
} else {
break;
}
}
// Check right
for (var x = startX + 1; x < GRID_SIZE; x++) {
var rightCandy = getCandyAt(x, startY);
if (rightCandy && rightCandy.candyType === type) {
horizontalMatches.push(rightCandy);
} else {
break;
}
}
if (horizontalMatches.length >= 3) {
matches = matches.concat(horizontalMatches);
}
// Check vertical matches
var verticalMatches = [candy];
// Check up
for (var y = startY - 1; y >= 0; y--) {
var upCandy = getCandyAt(startX, y);
if (upCandy && upCandy.candyType === type) {
verticalMatches.unshift(upCandy);
} else {
break;
}
}
// Check down
for (var y = startY + 1; y < GRID_SIZE; y++) {
var downCandy = getCandyAt(startX, y);
if (downCandy && downCandy.candyType === type) {
verticalMatches.push(downCandy);
} else {
break;
}
}
if (verticalMatches.length >= 3) {
matches = matches.concat(verticalMatches);
}
// Remove duplicates
var uniqueMatches = [];
for (var i = 0; i < matches.length; i++) {
var found = false;
for (var j = 0; j < uniqueMatches.length; j++) {
if (matches[i] === uniqueMatches[j]) {
found = true;
break;
}
}
if (!found) {
uniqueMatches.push(matches[i]);
}
}
return uniqueMatches;
}
function findAllMatches() {
var allMatches = [];
for (var x = 0; x < GRID_SIZE; x++) {
for (var y = 0; y < GRID_SIZE; y++) {
var matches = findMatches(x, y);
for (var i = 0; i < matches.length; i++) {
var found = false;
for (var j = 0; j < allMatches.length; j++) {
if (matches[i] === allMatches[j]) {
found = true;
break;
}
}
if (!found) {
allMatches.push(matches[i]);
}
}
}
}
return allMatches;
}
function createExplosionEffect(centerX, centerY, candyType) {
var candyAssets = ['candy_red', 'candy_blue', 'candy_green', 'candy_yellow', 'candy_purple', 'candy_orange'];
var particleCount = 8;
var colors = [0xff4757, 0x3742fa, 0x2ed573, 0xffa502, 0xa55eea, 0xff6348];
var particleColor = colors[candyType];
// Create multiple small particles
for (var i = 0; i < particleCount; i++) {
var particle = LK.getAsset('candy_' + ['red', 'blue', 'green', 'yellow', 'purple', 'orange'][candyType], {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.3
});
particle.x = centerX;
particle.y = centerY;
particle.alpha = 0.8;
game.addChild(particle);
// Calculate random direction for explosion
var angle = Math.PI * 2 / particleCount * i + (Math.random() - 0.5) * 0.5;
var distance = 80 + Math.random() * 40;
var targetX = centerX + Math.cos(angle) * distance;
var targetY = centerY + Math.sin(angle) * distance;
// Animate particle explosion
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 400 + Math.random() * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
}
function createBombExplosionEffect(centerX, centerY) {
var particleCount = 16;
// Create multiple dark particles for bomb explosion
for (var i = 0; i < particleCount; i++) {
var particle = LK.getAsset('bomb', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.2,
scaleY: 0.2
});
particle.x = centerX;
particle.y = centerY;
particle.alpha = 0.9;
particle.tint = 0xff4500; // Orange tint for explosion
game.addChild(particle);
// Calculate random direction for explosion
var angle = Math.PI * 2 / particleCount * i + (Math.random() - 0.5) * 0.8;
var distance = 120 + Math.random() * 80; // Larger explosion radius
var targetX = centerX + Math.cos(angle) * distance;
var targetY = centerY + Math.sin(angle) * distance;
// Animate particle explosion
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 600 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
}
function removeMatches(matches) {
if (matches.length === 0) {
return;
}
// Check for bombs in matches and add surrounding candies
var allCandieToRemove = [];
var hasBombs = false;
// Check if any candy in matches is a bomb OR if there's a bomb adjacent to matched candies
for (var i = 0; i < matches.length; i++) {
var candy = matches[i];
allCandieToRemove.push(candy);
if (candy.isBomb()) {
hasBombs = true;
// Add all surrounding candies to removal list
var surroundingCandies = getCandiesAroundBomb(candy.gridX, candy.gridY);
for (var j = 0; j < surroundingCandies.length; j++) {
var surroundingCandy = surroundingCandies[j];
// Check if not already in removal list
var alreadyAdded = false;
for (var k = 0; k < allCandieToRemove.length; k++) {
if (allCandieToRemove[k] === surroundingCandy) {
alreadyAdded = true;
break;
}
}
if (!alreadyAdded) {
allCandieToRemove.push(surroundingCandy);
}
}
} else {
// Check if there's a bomb adjacent to this matched candy
var adjacentPositions = [{
x: candy.gridX - 1,
y: candy.gridY
}, {
x: candy.gridX + 1,
y: candy.gridY
}, {
x: candy.gridX,
y: candy.gridY - 1
}, {
x: candy.gridX,
y: candy.gridY + 1
}];
for (var j = 0; j < adjacentPositions.length; j++) {
var pos = adjacentPositions[j];
var adjacentCandy = getCandyAt(pos.x, pos.y);
if (adjacentCandy && adjacentCandy.isBomb() && !adjacentCandy.isMatched) {
hasBombs = true;
// Add bomb and its surrounding candies
var alreadyAdded = false;
for (var k = 0; k < allCandieToRemove.length; k++) {
if (allCandieToRemove[k] === adjacentCandy) {
alreadyAdded = true;
break;
}
}
if (!alreadyAdded) {
allCandieToRemove.push(adjacentCandy);
var surroundingCandies = getCandiesAroundBomb(adjacentCandy.gridX, adjacentCandy.gridY);
for (var l = 0; l < surroundingCandies.length; l++) {
var surroundingCandy = surroundingCandies[l];
var bombAlreadyAdded = false;
for (var m = 0; m < allCandieToRemove.length; m++) {
if (allCandieToRemove[m] === surroundingCandy) {
bombAlreadyAdded = true;
break;
}
}
if (!bombAlreadyAdded) {
allCandieToRemove.push(surroundingCandy);
}
}
}
}
}
}
}
// Add screen shake effect when bombs explode
if (hasBombs) {
var originalX = game.x;
var originalY = game.y;
var shakeIntensity = 15;
var shakeDuration = 500;
// Create shake effect by tweening the game container back and forth
tween(game, {
x: originalX + shakeIntensity
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX - shakeIntensity
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX + shakeIntensity / 2
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX - shakeIntensity / 2
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX
}, {
duration: 100,
easing: tween.easeOut
});
}
});
}
});
}
});
}
});
}
// Calculate points based on total candies removed
var points = 0;
if (allCandieToRemove.length === 3) {
points = 100;
} else if (allCandieToRemove.length === 4) {
points = 200;
} else if (allCandieToRemove.length <= 6) {
points = 500;
} else {
points = 1000; // Bonus points for bomb explosions
}
score += points;
scoreText.setText('Score: ' + score);
// Play appropriate sound
if (hasBombs) {
LK.getSound('explosion').play();
} else {
LK.getSound('match').play();
}
// Flash effect for matches
if (hasBombs) {
LK.effects.flashScreen(0xff4500, 500); // Orange flash for bombs
} else if (allCandieToRemove.length >= 4) {
LK.effects.flashScreen(0xffd700, 300);
} else if (allCandieToRemove.length >= 3) {
LK.effects.flashScreen(0x2ed573, 200);
}
// Create explosion effects for each candy
for (var i = 0; i < allCandieToRemove.length; i++) {
var candy = allCandieToRemove[i];
if (candy.isBomb()) {
createBombExplosionEffect(candy.x, candy.y);
} else {
createExplosionEffect(candy.x, candy.y, candy.candyType);
}
}
// Animate candies disappearing
var animationsComplete = 0;
for (var i = 0; i < allCandieToRemove.length; i++) {
var candy = allCandieToRemove[i];
grid[candy.gridX][candy.gridY] = null;
// Use bomb explosion animation for bombs
if (candy.isBomb()) {
candy.explodeBomb(function () {
animationsComplete++;
if (animationsComplete === allCandieToRemove.length) {
applyGravity();
}
});
} else {
candy.animateMatch(function () {
animationsComplete++;
if (animationsComplete === allCandieToRemove.length) {
applyGravity();
}
});
}
}
}
function applyGravity() {
var needsGravity = false;
for (var x = 0; x < GRID_SIZE; x++) {
for (var y = GRID_SIZE - 1; y >= 0; y--) {
if (grid[x][y] === null) {
// Find candy above
for (var searchY = y - 1; searchY >= 0; searchY--) {
if (grid[x][searchY] !== null) {
var candy = grid[x][searchY];
grid[x][y] = candy;
grid[x][searchY] = null;
candy.gridY = y;
var targetY = GRID_START_Y + y * CELL_SIZE + CELL_SIZE / 2;
candy.animateToPosition(candy.x, targetY, 300);
needsGravity = true;
break;
}
}
}
}
}
// Spawn new candies
for (var x = 0; x < GRID_SIZE; x++) {
for (var y = 0; y < GRID_SIZE; y++) {
if (grid[x][y] === null) {
// Only spawn one bomb in the entire grid occasionally
var candyType;
var hasBombInGrid = false;
// Check if there's already a bomb in the grid
for (var checkX = 0; checkX < GRID_SIZE; checkX++) {
for (var checkY = 0; checkY < GRID_SIZE; checkY++) {
if (grid[checkX] && grid[checkX][checkY] && grid[checkX][checkY].candyType === 6) {
hasBombInGrid = true;
break;
}
}
if (hasBombInGrid) break;
}
// Only 1% chance for bomb if no bomb exists yet
if (!hasBombInGrid && Math.random() < 0.01) {
candyType = 6; // Bomb type
} else {
candyType = Math.floor(Math.random() * 6); // Regular candy types only
}
var candy = new Candy(candyType);
candy.setGridPosition(x, y);
candy.y = GRID_START_Y - CELL_SIZE;
var targetY = GRID_START_Y + y * CELL_SIZE + CELL_SIZE / 2;
candy.animateToPosition(candy.x, targetY, 400);
grid[x][y] = candy;
game.addChild(candy);
needsGravity = true;
}
}
}
if (needsGravity) {
LK.setTimeout(function () {
checkForMatches();
}, 500);
} else {
isProcessing = false;
}
}
function checkForMatches() {
var matches = findAllMatches();
if (matches.length > 0) {
removeMatches(matches);
} else {
isProcessing = false;
}
}
function isAdjacent(candy1, candy2) {
var dx = Math.abs(candy1.gridX - candy2.gridX);
var dy = Math.abs(candy1.gridY - candy2.gridY);
return dx === 1 && dy === 0 || dx === 0 && dy === 1;
}
function getCandyAtPosition(x, y) {
for (var gx = 0; gx < GRID_SIZE; gx++) {
for (var gy = 0; gy < GRID_SIZE; gy++) {
var candy = grid[gx][gy];
if (candy && !candy.isMatched) {
var candyBounds = {
x: candy.x - CELL_SIZE / 2,
y: candy.y - CELL_SIZE / 2,
width: CELL_SIZE,
height: CELL_SIZE
};
if (x >= candyBounds.x && x <= candyBounds.x + candyBounds.width && y >= candyBounds.y && y <= candyBounds.y + candyBounds.height) {
return candy;
}
}
}
}
return null;
}
// Game event handlers
game.down = function (x, y, obj) {
if (isProcessing) {
return;
}
var candy = getCandyAtPosition(x, y);
if (candy) {
if (selectedCandy && selectedCandy !== candy) {
selectedCandy.alpha = 1.0;
selectedCandy.scaleX = 1.0;
selectedCandy.scaleY = 1.0;
}
selectedCandy = candy;
draggedCandy = candy;
dragStartX = x;
dragStartY = y;
originalCandyX = candy.x;
originalCandyY = candy.y;
candy.alpha = 0.8;
candy.scaleX = 1.15;
candy.scaleY = 1.15;
LK.getSound('swap').play();
}
};
game.move = function (x, y, obj) {
if (isProcessing || !draggedCandy) {
return;
}
// Calculate drag distance
var deltaX = x - dragStartX;
var deltaY = y - dragStartY;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Only start visual dragging if moved enough distance
if (distance > 30) {
// Limit drag distance to adjacent cells
var maxDrag = CELL_SIZE * 0.7;
var dragX = Math.max(-maxDrag, Math.min(maxDrag, deltaX));
var dragY = Math.max(-maxDrag, Math.min(maxDrag, deltaY));
// Move the candy with the drag
draggedCandy.x = originalCandyX + dragX;
draggedCandy.y = originalCandyY + dragY;
}
};
game.up = function (x, y, obj) {
if (isProcessing || !selectedCandy) {
return;
}
var targetCandy = null;
var performedSwap = false;
if (draggedCandy) {
// Calculate drag direction and find target candy
var deltaX = x - dragStartX;
var deltaY = y - dragStartY;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > 60) {
// Determine which direction was dragged most
var targetGridX = draggedCandy.gridX;
var targetGridY = draggedCandy.gridY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal drag
if (deltaX > 0) {
targetGridX++; // Right
} else {
targetGridX--; // Left
}
} else {
// Vertical drag
if (deltaY > 0) {
targetGridY++; // Down
} else {
targetGridY--; // Up
}
}
targetCandy = getCandyAt(targetGridX, targetGridY);
} else {
// Small drag, check position under finger
targetCandy = getCandyAtPosition(x, y);
}
} else {
// No drag, check position under finger
targetCandy = getCandyAtPosition(x, y);
}
if (targetCandy && targetCandy !== selectedCandy && isAdjacent(selectedCandy, targetCandy)) {
isProcessing = true;
performedSwap = true;
// Try the swap
var candy1 = selectedCandy;
var candy2 = targetCandy;
if (swapCandies(candy1, candy2)) {
LK.getSound('swap').play();
// Animate the swap
var targetX1 = candy1.x;
var targetY1 = candy1.y;
var targetX2 = candy2.x;
var targetY2 = candy2.y;
// Reset candy1 position first if it was dragged
if (draggedCandy === candy1) {
candy1.x = originalCandyX;
candy1.y = originalCandyY;
}
candy1.animateToPosition(targetX1, targetY1, 200);
candy2.animateToPosition(targetX2, targetY2, 200, function () {
// Check for matches after swap
var matches1 = findMatches(candy1.gridX, candy1.gridY);
var matches2 = findMatches(candy2.gridX, candy2.gridY);
var allSwapMatches = matches1.concat(matches2);
// Remove duplicates
var uniqueMatches = [];
for (var i = 0; i < allSwapMatches.length; i++) {
var found = false;
for (var j = 0; j < uniqueMatches.length; j++) {
if (allSwapMatches[i] === uniqueMatches[j]) {
found = true;
break;
}
}
if (!found) {
uniqueMatches.push(allSwapMatches[i]);
}
}
if (uniqueMatches.length > 0) {
moves--;
movesText.setText('Moves: ' + moves);
removeMatches(uniqueMatches);
} else {
// Invalid move, swap back
swapCandies(candy1, candy2);
candy1.animateToPosition(targetX2, targetY2, 200);
candy2.animateToPosition(targetX1, targetY1, 200, function () {
isProcessing = false;
});
}
});
} else {
isProcessing = false;
}
}
// Reset dragged candy position if no valid swap was made
if (draggedCandy && !performedSwap) {
draggedCandy.animateToPosition(originalCandyX, originalCandyY, 200);
}
if (selectedCandy) {
selectedCandy.alpha = 1.0;
selectedCandy.scaleX = 1.0;
selectedCandy.scaleY = 1.0;
selectedCandy = null;
}
// Reset drag variables
draggedCandy = null;
dragStartX = 0;
dragStartY = 0;
originalCandyX = 0;
originalCandyY = 0;
};
// Game update loop
game.update = function () {
// Check win condition
if (score >= targetScore) {
LK.showYouWin();
}
// Check lose condition
if (moves <= 0 && score < targetScore) {
LK.showGameOver();
}
};
// Initialize the game
initializeGrid(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Candy = Container.expand(function (type) {
var self = Container.call(this);
self.candyType = type;
self.gridX = 0;
self.gridY = 0;
self.isAnimating = false;
self.isMatched = false;
var candyAssets = ['candy_red', 'candy_blue', 'candy_green', 'candy_yellow', 'candy_purple', 'candy_orange', 'bomb'];
var candyGraphics = self.attachAsset(candyAssets[type], {
anchorX: 0.5,
anchorY: 0.5
});
self.setGridPosition = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = GRID_START_X + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = GRID_START_Y + gridY * CELL_SIZE + CELL_SIZE / 2;
};
self.animateToPosition = function (targetX, targetY, duration, onComplete) {
self.isAnimating = true;
tween(self, {
x: targetX,
y: targetY
}, {
duration: duration || 300,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isAnimating = false;
if (onComplete) {
onComplete();
}
}
});
};
self.animateMatch = function (onComplete) {
self.isMatched = true;
tween(self, {
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: onComplete
});
};
self.explodeBomb = function (onComplete) {
self.isMatched = true;
// Scale up and fade out for bomb explosion
tween(self, {
scaleX: 3,
scaleY: 3,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: onComplete
});
};
self.isBomb = function () {
return self.candyType === 6; // Bomb is type 6
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1A1A2E
});
/****
* Game Code
****/
// Game constants
var GRID_SIZE = 8;
var CELL_SIZE = 180;
var GRID_START_X = (2048 - GRID_SIZE * CELL_SIZE) / 2;
var GRID_START_Y = 500;
var CANDY_TYPES = 7; // 6 regular candies + 1 bomb type
// Game variables
var grid = [];
var selectedCandy = null;
var isProcessing = false;
var score = 0;
var moves = 30;
var targetScore = 5000;
var draggedCandy = null;
var dragStartX = 0;
var dragStartY = 0;
var originalCandyX = 0;
var originalCandyY = 0;
// Initialize UI
var scoreText = new Text2('Score: 0', {
size: 90,
fill: '#FFFFFF'
});
scoreText.anchor.set(0.5, 0);
scoreText.y = 50;
LK.gui.top.addChild(scoreText);
var movesText = new Text2('Moves: 30', {
size: 70,
fill: '#FFD700'
});
movesText.anchor.set(0.5, 0);
movesText.y = 150;
LK.gui.top.addChild(movesText);
var targetText = new Text2('Target: 5000', {
size: 70,
fill: '#FF6B6B'
});
targetText.anchor.set(0.5, 0);
targetText.y = 220;
LK.gui.top.addChild(targetText);
// Initialize grid
function initializeGrid() {
grid = [];
for (var x = 0; x < GRID_SIZE; x++) {
grid[x] = [];
for (var y = 0; y < GRID_SIZE; y++) {
// Only spawn one bomb in the entire grid occasionally
var candyType;
var hasBombInGrid = false;
// Check if there's already a bomb in the grid
for (var checkX = 0; checkX < GRID_SIZE; checkX++) {
for (var checkY = 0; checkY < GRID_SIZE; checkY++) {
if (grid[checkX] && grid[checkX][checkY] && grid[checkX][checkY].candyType === 6) {
hasBombInGrid = true;
break;
}
}
if (hasBombInGrid) break;
}
// Only 2% chance for bomb if no bomb exists yet
if (!hasBombInGrid && Math.random() < 0.02) {
candyType = 6; // Bomb type
} else {
candyType = Math.floor(Math.random() * 6); // Regular candy types only
}
var candy = new Candy(candyType);
candy.setGridPosition(x, y);
grid[x][y] = candy;
game.addChild(candy);
}
}
// Remove initial matches
removeInitialMatches();
}
function removeInitialMatches() {
var hasMatches = true;
while (hasMatches) {
var matches = findAllMatches();
if (matches.length === 0) {
hasMatches = false;
} else {
for (var i = 0; i < matches.length; i++) {
var match = matches[i];
var newType = Math.floor(Math.random() * CANDY_TYPES);
match.candyType = newType;
match.removeChild(match.children[0]);
var candyAssets = ['candy_red', 'candy_blue', 'candy_green', 'candy_yellow', 'candy_purple', 'candy_orange', 'bomb'];
match.attachAsset(candyAssets[newType], {
anchorX: 0.5,
anchorY: 0.5
});
}
}
}
}
function getCandyAt(x, y) {
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) {
return null;
}
return grid[x][y];
}
function getCandiesAroundBomb(bombX, bombY) {
var surroundingCandies = [];
// Get all 8 surrounding cells plus the bomb itself
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
var candy = getCandyAt(bombX + dx, bombY + dy);
if (candy && !candy.isMatched) {
surroundingCandies.push(candy);
}
}
}
return surroundingCandies;
}
function swapCandies(candy1, candy2) {
if (!candy1 || !candy2 || candy1.isAnimating || candy2.isAnimating) {
return false;
}
var tempX = candy1.gridX;
var tempY = candy1.gridY;
// Update grid positions
grid[candy1.gridX][candy1.gridY] = candy2;
grid[candy2.gridX][candy2.gridY] = candy1;
// Update candy grid positions
candy1.setGridPosition(candy2.gridX, candy2.gridY);
candy2.setGridPosition(tempX, tempY);
return true;
}
function findMatches(startX, startY) {
var candy = getCandyAt(startX, startY);
if (!candy) {
return [];
}
var matches = [];
var type = candy.candyType;
// Check horizontal matches
var horizontalMatches = [candy];
// Check left
for (var x = startX - 1; x >= 0; x--) {
var leftCandy = getCandyAt(x, startY);
if (leftCandy && leftCandy.candyType === type) {
horizontalMatches.unshift(leftCandy);
} else {
break;
}
}
// Check right
for (var x = startX + 1; x < GRID_SIZE; x++) {
var rightCandy = getCandyAt(x, startY);
if (rightCandy && rightCandy.candyType === type) {
horizontalMatches.push(rightCandy);
} else {
break;
}
}
if (horizontalMatches.length >= 3) {
matches = matches.concat(horizontalMatches);
}
// Check vertical matches
var verticalMatches = [candy];
// Check up
for (var y = startY - 1; y >= 0; y--) {
var upCandy = getCandyAt(startX, y);
if (upCandy && upCandy.candyType === type) {
verticalMatches.unshift(upCandy);
} else {
break;
}
}
// Check down
for (var y = startY + 1; y < GRID_SIZE; y++) {
var downCandy = getCandyAt(startX, y);
if (downCandy && downCandy.candyType === type) {
verticalMatches.push(downCandy);
} else {
break;
}
}
if (verticalMatches.length >= 3) {
matches = matches.concat(verticalMatches);
}
// Remove duplicates
var uniqueMatches = [];
for (var i = 0; i < matches.length; i++) {
var found = false;
for (var j = 0; j < uniqueMatches.length; j++) {
if (matches[i] === uniqueMatches[j]) {
found = true;
break;
}
}
if (!found) {
uniqueMatches.push(matches[i]);
}
}
return uniqueMatches;
}
function findAllMatches() {
var allMatches = [];
for (var x = 0; x < GRID_SIZE; x++) {
for (var y = 0; y < GRID_SIZE; y++) {
var matches = findMatches(x, y);
for (var i = 0; i < matches.length; i++) {
var found = false;
for (var j = 0; j < allMatches.length; j++) {
if (matches[i] === allMatches[j]) {
found = true;
break;
}
}
if (!found) {
allMatches.push(matches[i]);
}
}
}
}
return allMatches;
}
function createExplosionEffect(centerX, centerY, candyType) {
var candyAssets = ['candy_red', 'candy_blue', 'candy_green', 'candy_yellow', 'candy_purple', 'candy_orange'];
var particleCount = 8;
var colors = [0xff4757, 0x3742fa, 0x2ed573, 0xffa502, 0xa55eea, 0xff6348];
var particleColor = colors[candyType];
// Create multiple small particles
for (var i = 0; i < particleCount; i++) {
var particle = LK.getAsset('candy_' + ['red', 'blue', 'green', 'yellow', 'purple', 'orange'][candyType], {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.3
});
particle.x = centerX;
particle.y = centerY;
particle.alpha = 0.8;
game.addChild(particle);
// Calculate random direction for explosion
var angle = Math.PI * 2 / particleCount * i + (Math.random() - 0.5) * 0.5;
var distance = 80 + Math.random() * 40;
var targetX = centerX + Math.cos(angle) * distance;
var targetY = centerY + Math.sin(angle) * distance;
// Animate particle explosion
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 400 + Math.random() * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
}
function createBombExplosionEffect(centerX, centerY) {
var particleCount = 16;
// Create multiple dark particles for bomb explosion
for (var i = 0; i < particleCount; i++) {
var particle = LK.getAsset('bomb', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.2,
scaleY: 0.2
});
particle.x = centerX;
particle.y = centerY;
particle.alpha = 0.9;
particle.tint = 0xff4500; // Orange tint for explosion
game.addChild(particle);
// Calculate random direction for explosion
var angle = Math.PI * 2 / particleCount * i + (Math.random() - 0.5) * 0.8;
var distance = 120 + Math.random() * 80; // Larger explosion radius
var targetX = centerX + Math.cos(angle) * distance;
var targetY = centerY + Math.sin(angle) * distance;
// Animate particle explosion
tween(particle, {
x: targetX,
y: targetY,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 600 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
particle.destroy();
}
});
}
}
function removeMatches(matches) {
if (matches.length === 0) {
return;
}
// Check for bombs in matches and add surrounding candies
var allCandieToRemove = [];
var hasBombs = false;
// Check if any candy in matches is a bomb OR if there's a bomb adjacent to matched candies
for (var i = 0; i < matches.length; i++) {
var candy = matches[i];
allCandieToRemove.push(candy);
if (candy.isBomb()) {
hasBombs = true;
// Add all surrounding candies to removal list
var surroundingCandies = getCandiesAroundBomb(candy.gridX, candy.gridY);
for (var j = 0; j < surroundingCandies.length; j++) {
var surroundingCandy = surroundingCandies[j];
// Check if not already in removal list
var alreadyAdded = false;
for (var k = 0; k < allCandieToRemove.length; k++) {
if (allCandieToRemove[k] === surroundingCandy) {
alreadyAdded = true;
break;
}
}
if (!alreadyAdded) {
allCandieToRemove.push(surroundingCandy);
}
}
} else {
// Check if there's a bomb adjacent to this matched candy
var adjacentPositions = [{
x: candy.gridX - 1,
y: candy.gridY
}, {
x: candy.gridX + 1,
y: candy.gridY
}, {
x: candy.gridX,
y: candy.gridY - 1
}, {
x: candy.gridX,
y: candy.gridY + 1
}];
for (var j = 0; j < adjacentPositions.length; j++) {
var pos = adjacentPositions[j];
var adjacentCandy = getCandyAt(pos.x, pos.y);
if (adjacentCandy && adjacentCandy.isBomb() && !adjacentCandy.isMatched) {
hasBombs = true;
// Add bomb and its surrounding candies
var alreadyAdded = false;
for (var k = 0; k < allCandieToRemove.length; k++) {
if (allCandieToRemove[k] === adjacentCandy) {
alreadyAdded = true;
break;
}
}
if (!alreadyAdded) {
allCandieToRemove.push(adjacentCandy);
var surroundingCandies = getCandiesAroundBomb(adjacentCandy.gridX, adjacentCandy.gridY);
for (var l = 0; l < surroundingCandies.length; l++) {
var surroundingCandy = surroundingCandies[l];
var bombAlreadyAdded = false;
for (var m = 0; m < allCandieToRemove.length; m++) {
if (allCandieToRemove[m] === surroundingCandy) {
bombAlreadyAdded = true;
break;
}
}
if (!bombAlreadyAdded) {
allCandieToRemove.push(surroundingCandy);
}
}
}
}
}
}
}
// Add screen shake effect when bombs explode
if (hasBombs) {
var originalX = game.x;
var originalY = game.y;
var shakeIntensity = 15;
var shakeDuration = 500;
// Create shake effect by tweening the game container back and forth
tween(game, {
x: originalX + shakeIntensity
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX - shakeIntensity
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX + shakeIntensity / 2
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX - shakeIntensity / 2
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(game, {
x: originalX
}, {
duration: 100,
easing: tween.easeOut
});
}
});
}
});
}
});
}
});
}
// Calculate points based on total candies removed
var points = 0;
if (allCandieToRemove.length === 3) {
points = 100;
} else if (allCandieToRemove.length === 4) {
points = 200;
} else if (allCandieToRemove.length <= 6) {
points = 500;
} else {
points = 1000; // Bonus points for bomb explosions
}
score += points;
scoreText.setText('Score: ' + score);
// Play appropriate sound
if (hasBombs) {
LK.getSound('explosion').play();
} else {
LK.getSound('match').play();
}
// Flash effect for matches
if (hasBombs) {
LK.effects.flashScreen(0xff4500, 500); // Orange flash for bombs
} else if (allCandieToRemove.length >= 4) {
LK.effects.flashScreen(0xffd700, 300);
} else if (allCandieToRemove.length >= 3) {
LK.effects.flashScreen(0x2ed573, 200);
}
// Create explosion effects for each candy
for (var i = 0; i < allCandieToRemove.length; i++) {
var candy = allCandieToRemove[i];
if (candy.isBomb()) {
createBombExplosionEffect(candy.x, candy.y);
} else {
createExplosionEffect(candy.x, candy.y, candy.candyType);
}
}
// Animate candies disappearing
var animationsComplete = 0;
for (var i = 0; i < allCandieToRemove.length; i++) {
var candy = allCandieToRemove[i];
grid[candy.gridX][candy.gridY] = null;
// Use bomb explosion animation for bombs
if (candy.isBomb()) {
candy.explodeBomb(function () {
animationsComplete++;
if (animationsComplete === allCandieToRemove.length) {
applyGravity();
}
});
} else {
candy.animateMatch(function () {
animationsComplete++;
if (animationsComplete === allCandieToRemove.length) {
applyGravity();
}
});
}
}
}
function applyGravity() {
var needsGravity = false;
for (var x = 0; x < GRID_SIZE; x++) {
for (var y = GRID_SIZE - 1; y >= 0; y--) {
if (grid[x][y] === null) {
// Find candy above
for (var searchY = y - 1; searchY >= 0; searchY--) {
if (grid[x][searchY] !== null) {
var candy = grid[x][searchY];
grid[x][y] = candy;
grid[x][searchY] = null;
candy.gridY = y;
var targetY = GRID_START_Y + y * CELL_SIZE + CELL_SIZE / 2;
candy.animateToPosition(candy.x, targetY, 300);
needsGravity = true;
break;
}
}
}
}
}
// Spawn new candies
for (var x = 0; x < GRID_SIZE; x++) {
for (var y = 0; y < GRID_SIZE; y++) {
if (grid[x][y] === null) {
// Only spawn one bomb in the entire grid occasionally
var candyType;
var hasBombInGrid = false;
// Check if there's already a bomb in the grid
for (var checkX = 0; checkX < GRID_SIZE; checkX++) {
for (var checkY = 0; checkY < GRID_SIZE; checkY++) {
if (grid[checkX] && grid[checkX][checkY] && grid[checkX][checkY].candyType === 6) {
hasBombInGrid = true;
break;
}
}
if (hasBombInGrid) break;
}
// Only 1% chance for bomb if no bomb exists yet
if (!hasBombInGrid && Math.random() < 0.01) {
candyType = 6; // Bomb type
} else {
candyType = Math.floor(Math.random() * 6); // Regular candy types only
}
var candy = new Candy(candyType);
candy.setGridPosition(x, y);
candy.y = GRID_START_Y - CELL_SIZE;
var targetY = GRID_START_Y + y * CELL_SIZE + CELL_SIZE / 2;
candy.animateToPosition(candy.x, targetY, 400);
grid[x][y] = candy;
game.addChild(candy);
needsGravity = true;
}
}
}
if (needsGravity) {
LK.setTimeout(function () {
checkForMatches();
}, 500);
} else {
isProcessing = false;
}
}
function checkForMatches() {
var matches = findAllMatches();
if (matches.length > 0) {
removeMatches(matches);
} else {
isProcessing = false;
}
}
function isAdjacent(candy1, candy2) {
var dx = Math.abs(candy1.gridX - candy2.gridX);
var dy = Math.abs(candy1.gridY - candy2.gridY);
return dx === 1 && dy === 0 || dx === 0 && dy === 1;
}
function getCandyAtPosition(x, y) {
for (var gx = 0; gx < GRID_SIZE; gx++) {
for (var gy = 0; gy < GRID_SIZE; gy++) {
var candy = grid[gx][gy];
if (candy && !candy.isMatched) {
var candyBounds = {
x: candy.x - CELL_SIZE / 2,
y: candy.y - CELL_SIZE / 2,
width: CELL_SIZE,
height: CELL_SIZE
};
if (x >= candyBounds.x && x <= candyBounds.x + candyBounds.width && y >= candyBounds.y && y <= candyBounds.y + candyBounds.height) {
return candy;
}
}
}
}
return null;
}
// Game event handlers
game.down = function (x, y, obj) {
if (isProcessing) {
return;
}
var candy = getCandyAtPosition(x, y);
if (candy) {
if (selectedCandy && selectedCandy !== candy) {
selectedCandy.alpha = 1.0;
selectedCandy.scaleX = 1.0;
selectedCandy.scaleY = 1.0;
}
selectedCandy = candy;
draggedCandy = candy;
dragStartX = x;
dragStartY = y;
originalCandyX = candy.x;
originalCandyY = candy.y;
candy.alpha = 0.8;
candy.scaleX = 1.15;
candy.scaleY = 1.15;
LK.getSound('swap').play();
}
};
game.move = function (x, y, obj) {
if (isProcessing || !draggedCandy) {
return;
}
// Calculate drag distance
var deltaX = x - dragStartX;
var deltaY = y - dragStartY;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Only start visual dragging if moved enough distance
if (distance > 30) {
// Limit drag distance to adjacent cells
var maxDrag = CELL_SIZE * 0.7;
var dragX = Math.max(-maxDrag, Math.min(maxDrag, deltaX));
var dragY = Math.max(-maxDrag, Math.min(maxDrag, deltaY));
// Move the candy with the drag
draggedCandy.x = originalCandyX + dragX;
draggedCandy.y = originalCandyY + dragY;
}
};
game.up = function (x, y, obj) {
if (isProcessing || !selectedCandy) {
return;
}
var targetCandy = null;
var performedSwap = false;
if (draggedCandy) {
// Calculate drag direction and find target candy
var deltaX = x - dragStartX;
var deltaY = y - dragStartY;
var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > 60) {
// Determine which direction was dragged most
var targetGridX = draggedCandy.gridX;
var targetGridY = draggedCandy.gridY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal drag
if (deltaX > 0) {
targetGridX++; // Right
} else {
targetGridX--; // Left
}
} else {
// Vertical drag
if (deltaY > 0) {
targetGridY++; // Down
} else {
targetGridY--; // Up
}
}
targetCandy = getCandyAt(targetGridX, targetGridY);
} else {
// Small drag, check position under finger
targetCandy = getCandyAtPosition(x, y);
}
} else {
// No drag, check position under finger
targetCandy = getCandyAtPosition(x, y);
}
if (targetCandy && targetCandy !== selectedCandy && isAdjacent(selectedCandy, targetCandy)) {
isProcessing = true;
performedSwap = true;
// Try the swap
var candy1 = selectedCandy;
var candy2 = targetCandy;
if (swapCandies(candy1, candy2)) {
LK.getSound('swap').play();
// Animate the swap
var targetX1 = candy1.x;
var targetY1 = candy1.y;
var targetX2 = candy2.x;
var targetY2 = candy2.y;
// Reset candy1 position first if it was dragged
if (draggedCandy === candy1) {
candy1.x = originalCandyX;
candy1.y = originalCandyY;
}
candy1.animateToPosition(targetX1, targetY1, 200);
candy2.animateToPosition(targetX2, targetY2, 200, function () {
// Check for matches after swap
var matches1 = findMatches(candy1.gridX, candy1.gridY);
var matches2 = findMatches(candy2.gridX, candy2.gridY);
var allSwapMatches = matches1.concat(matches2);
// Remove duplicates
var uniqueMatches = [];
for (var i = 0; i < allSwapMatches.length; i++) {
var found = false;
for (var j = 0; j < uniqueMatches.length; j++) {
if (allSwapMatches[i] === uniqueMatches[j]) {
found = true;
break;
}
}
if (!found) {
uniqueMatches.push(allSwapMatches[i]);
}
}
if (uniqueMatches.length > 0) {
moves--;
movesText.setText('Moves: ' + moves);
removeMatches(uniqueMatches);
} else {
// Invalid move, swap back
swapCandies(candy1, candy2);
candy1.animateToPosition(targetX2, targetY2, 200);
candy2.animateToPosition(targetX1, targetY1, 200, function () {
isProcessing = false;
});
}
});
} else {
isProcessing = false;
}
}
// Reset dragged candy position if no valid swap was made
if (draggedCandy && !performedSwap) {
draggedCandy.animateToPosition(originalCandyX, originalCandyY, 200);
}
if (selectedCandy) {
selectedCandy.alpha = 1.0;
selectedCandy.scaleX = 1.0;
selectedCandy.scaleY = 1.0;
selectedCandy = null;
}
// Reset drag variables
draggedCandy = null;
dragStartX = 0;
dragStartY = 0;
originalCandyX = 0;
originalCandyY = 0;
};
// Game update loop
game.update = function () {
// Check win condition
if (score >= targetScore) {
LK.showYouWin();
}
// Check lose condition
if (moves <= 0 && score < targetScore) {
LK.showGameOver();
}
};
// Initialize the game
initializeGrid();