Code edit (1 edits merged)
Please save this source code
User prompt
Candy Match Mania
Initial prompt
Create a Candy Crush-style match-3 puzzle game with a vibrant candy-themed design. The game should feature a grid of colorful candies (e.g., striped, wrapped, bomb, and rainbow variants) that players swap horizontally or vertically to match 3 or more of the same type. Include mechanics like level progression with increasing difficulty, unique objectives (e.g., clear jelly, collect specific candies, reach a target score), and obstacles (e.g., chocolate blockers, ice barriers, timed bombs). Add power-ups generated by matching 4+ candies (e.g., striped candy blasts, color bombs that clear all candies of a chosen color). Implement a lives system, daily rewards, and a boosters shop using in-game currency. Ensure smooth animations for candy swaps, matches, and explosions, along with cheerful sound effects and background music. The game must be mobile-responsive with touch controls and include a level editor for future content updates
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Candy class
var Candy = Container.expand(function () {
var self = Container.call(this);
// Properties
self.type = getRandomCandyType();
self.special = SPECIAL_NONE;
self.blocker = BLOCKER_NONE;
self.row = 0;
self.col = 0;
self.isFalling = false;
self.isMatched = false;
self.isSelected = false;
self.asset = null;
// Attach asset
function updateAsset() {
if (self.asset) {
self.removeChild(self.asset);
self.asset.destroy();
}
var assetId = self.type;
if (self.special === SPECIAL_STRIPED) assetId = 'candy_striped';
if (self.special === SPECIAL_BOMB) assetId = 'candy_bomb';
if (self.special === SPECIAL_RAINBOW) assetId = 'candy_rainbow';
if (self.blocker === BLOCKER_CHOCOLATE) assetId = 'blocker_chocolate';
if (self.blocker === BLOCKER_ICE) assetId = 'blocker_ice';
self.asset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
if (self.isSelected) {
self.asset.scaleX = 1.15;
self.asset.scaleY = 1.15;
} else {
self.asset.scaleX = 1;
self.asset.scaleY = 1;
}
}
// Set type
self.setType = function (type, special, blocker) {
self.type = type || getRandomCandyType();
self.special = special || SPECIAL_NONE;
self.blocker = blocker || BLOCKER_NONE;
updateAsset();
};
// Set selected
self.setSelected = function (selected) {
self.isSelected = selected;
updateAsset();
};
// Animate to position
self.moveTo = function (x, y, duration, onFinish) {
tween(self, {
x: x,
y: y
}, {
duration: duration || 200,
easing: tween.easeInOut,
onFinish: onFinish
});
};
// Destroy
self.destroyCandy = function () {
if (self.asset) {
self.removeChild(self.asset);
self.asset.destroy();
self.asset = null;
}
self.destroy();
};
// Init
updateAsset();
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222244
});
/****
* Game Code
****/
// Candy types
// Music
// Sounds
// Blockers
// Special candies
// Candy shapes/colors
// Board data
var CANDY_TYPES = ['candy_red', 'candy_green', 'candy_blue', 'candy_yellow', 'candy_purple', 'candy_orange'];
// Special types
var SPECIAL_NONE = 0;
var SPECIAL_STRIPED = 1;
var SPECIAL_BOMB = 2;
var SPECIAL_RAINBOW = 3;
// Blocker types
var BLOCKER_NONE = 0;
var BLOCKER_CHOCOLATE = 1;
var BLOCKER_ICE = 2;
// Board size
var BOARD_COLS = 7;
var BOARD_ROWS = 9;
var CELL_SIZE = 200;
var BOARD_OFFSET_X = Math.floor((2048 - BOARD_COLS * CELL_SIZE) / 2);
var BOARD_OFFSET_Y = 300;
// Helper: get random candy type
function getRandomCandyType() {
return CANDY_TYPES[Math.floor(Math.random() * CANDY_TYPES.length)];
}
// Helper: get random int
function randInt(a, b) {
return a + Math.floor(Math.random() * (b - a + 1));
}
var board = [];
var candies = [];
var selectedCandy = null;
var swapping = false;
var animating = false;
var movesLeft = 20;
var targetScore = 5000;
var score = 0;
var scoreTxt = null;
var movesTxt = null;
var targetTxt = null;
var boardContainer = null;
var matchQueue = [];
var refillQueue = [];
var isProcessing = false;
// GUI
scoreTxt = new Text2('Score: 0', {
size: 90,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
movesTxt = new Text2('Moves: 20', {
size: 70,
fill: "#fff"
});
movesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(movesTxt);
movesTxt.y = 110;
targetTxt = new Text2('Target: 5000', {
size: 60,
fill: "#fff"
});
targetTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(targetTxt);
targetTxt.y = 180;
// Board container
boardContainer = new Container();
game.addChild(boardContainer);
boardContainer.x = BOARD_OFFSET_X;
boardContainer.y = BOARD_OFFSET_Y;
// Initialize board
function initBoard() {
// Clear previous
for (var i = 0; i < candies.length; ++i) {
if (candies[i]) candies[i].destroyCandy();
}
candies = [];
board = [];
for (var row = 0; row < BOARD_ROWS; ++row) {
board[row] = [];
for (var col = 0; col < BOARD_COLS; ++col) {
var candy = new Candy();
candy.row = row;
candy.col = col;
candy.x = col * CELL_SIZE + CELL_SIZE / 2;
candy.y = row * CELL_SIZE + CELL_SIZE / 2;
boardContainer.addChild(candy);
candies.push(candy);
board[row][col] = candy;
}
}
// Remove initial matches
removeInitialMatches();
}
// Remove initial matches to avoid auto-matches at start
function removeInitialMatches() {
for (var row = 0; row < BOARD_ROWS; ++row) {
for (var col = 0; col < BOARD_COLS; ++col) {
var type = board[row][col].type;
// Check left
if (col >= 2 && board[row][col - 1].type === type && board[row][col - 2].type === type) {
var newType = getRandomCandyType();
while (newType === type) newType = getRandomCandyType();
board[row][col].setType(newType);
}
// Check up
if (row >= 2 && board[row - 1][col].type === type && board[row - 2][col].type === type) {
var newType = getRandomCandyType();
while (newType === type) newType = getRandomCandyType();
board[row][col].setType(newType);
}
}
}
}
// Get candy at board position
function getCandyAt(row, col) {
if (row < 0 || row >= BOARD_ROWS || col < 0 || col >= BOARD_COLS) return null;
return board[row][col];
}
// Swap two candies
function swapCandies(c1, c2, cb) {
swapping = true;
var r1 = c1.row,
c1c = c1.col,
r2 = c2.row,
c2c = c2.col;
// Swap in board
board[r1][c1c] = c2;
board[r2][c2c] = c1;
// Swap row/col
var tmpRow = c1.row,
tmpCol = c1.col;
c1.row = c2.row;
c1.col = c2.col;
c2.row = tmpRow;
c2.col = tmpCol;
// Animate
var done = 0;
c1.moveTo(c1.col * CELL_SIZE + CELL_SIZE / 2, c1.row * CELL_SIZE + CELL_SIZE / 2, 180, function () {
done++;
if (done === 2 && cb) cb();
});
c2.moveTo(c2.col * CELL_SIZE + CELL_SIZE / 2, c2.row * CELL_SIZE + CELL_SIZE / 2, 180, function () {
done++;
if (done === 2 && cb) cb();
});
LK.getSound('swap').play();
}
// Check if two candies are adjacent
function areAdjacent(c1, c2) {
var dr = Math.abs(c1.row - c2.row);
var dc = Math.abs(c1.col - c2.col);
return dr + dc === 1;
}
// Find all matches on the board
function findMatches() {
var matches = [];
// Horizontal
for (var row = 0; row < BOARD_ROWS; ++row) {
var count = 1;
for (var col = 1; col < BOARD_COLS; ++col) {
var prev = board[row][col - 1];
var curr = board[row][col];
if (curr.type === prev.type && curr.blocker === BLOCKER_NONE && prev.blocker === BLOCKER_NONE) {
count++;
} else {
if (count >= 3) {
for (var k = 0; k < count; ++k) {
matches.push(board[row][col - 1 - k]);
}
}
count = 1;
}
}
if (count >= 3) {
for (var k = 0; k < count; ++k) {
matches.push(board[row][BOARD_COLS - 1 - k]);
}
}
}
// Vertical
for (var col = 0; col < BOARD_COLS; ++col) {
var count = 1;
for (var row = 1; row < BOARD_ROWS; ++row) {
var prev = board[row - 1][col];
var curr = board[row][col];
if (curr.type === prev.type && curr.blocker === BLOCKER_NONE && prev.blocker === BLOCKER_NONE) {
count++;
} else {
if (count >= 3) {
for (var k = 0; k < count; ++k) {
matches.push(board[row - 1 - k][col]);
}
}
count = 1;
}
}
if (count >= 3) {
for (var k = 0; k < count; ++k) {
matches.push(board[BOARD_ROWS - 1 - k][col]);
}
}
}
// Remove duplicates
var unique = [];
for (var i = 0; i < matches.length; ++i) {
if (unique.indexOf(matches[i]) === -1) unique.push(matches[i]);
}
return unique;
}
// Remove matched candies and animate
function removeMatches(matches, cb) {
if (!matches || matches.length === 0) {
if (cb) cb();
return;
}
LK.getSound('match').play();
var done = 0;
for (var i = 0; i < matches.length; ++i) {
var candy = matches[i];
candy.isMatched = true;
// Animate scale down and fade
tween(candy, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function (candy) {
return function () {
if (candy.asset) {
candy.removeChild(candy.asset);
candy.asset.destroy();
candy.asset = null;
}
candy.visible = false;
done++;
if (done === matches.length && cb) cb();
};
}(candy)
});
// Add score
score += 100;
}
}
// Drop candies to fill empty spaces
function dropCandies(cb) {
var moved = false;
for (var col = 0; col < BOARD_COLS; ++col) {
for (var row = BOARD_ROWS - 1; row >= 0; --row) {
var candy = board[row][col];
if (!candy.isMatched && candy.visible) continue;
// Find nearest above
for (var above = row - 1; above >= 0; --above) {
var aboveCandy = board[above][col];
if (!aboveCandy.isMatched && aboveCandy.visible) {
// Move aboveCandy down
board[row][col] = aboveCandy;
aboveCandy.row = row;
aboveCandy.col = col;
aboveCandy.moveTo(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 180);
board[above][col] = candy;
moved = true;
break;
}
}
}
}
if (cb) LK.setTimeout(cb, moved ? 200 : 0);
}
// Fill empty spaces with new candies
function refillBoard(cb) {
var created = false;
for (var col = 0; col < BOARD_COLS; ++col) {
for (var row = 0; row < BOARD_ROWS; ++row) {
var candy = board[row][col];
if (!candy.isMatched && candy.visible) continue;
// Create new candy
var newCandy = new Candy();
newCandy.row = row;
newCandy.col = col;
newCandy.x = col * CELL_SIZE + CELL_SIZE / 2;
newCandy.y = -CELL_SIZE + CELL_SIZE / 2;
boardContainer.addChild(newCandy);
candies.push(newCandy);
board[row][col] = newCandy;
newCandy.moveTo(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 220);
created = true;
}
}
if (cb) LK.setTimeout(cb, created ? 220 : 0);
}
// Remove matched candies from board
function clearMatchedCandies() {
for (var row = 0; row < BOARD_ROWS; ++row) {
for (var col = 0; col < BOARD_COLS; ++col) {
var candy = board[row][col];
if (candy.isMatched) {
candy.destroyCandy();
// Remove from candies array
for (var i = 0; i < candies.length; ++i) {
if (candies[i] === candy) {
candies.splice(i, 1);
break;
}
}
// Replace with dummy invisible candy for drop logic
var dummy = new Candy();
dummy.row = row;
dummy.col = col;
dummy.visible = false;
board[row][col] = dummy;
}
}
}
}
// Deselect all candies
function deselectAll() {
for (var i = 0; i < candies.length; ++i) {
candies[i].setSelected(false);
}
selectedCandy = null;
}
// Handle user tap
function handleTap(x, y, obj) {
if (swapping || animating || isProcessing) return;
// Convert to board coordinates
var local = boardContainer.toLocal({
x: x,
y: y
});
var col = Math.floor(local.x / CELL_SIZE);
var row = Math.floor(local.y / CELL_SIZE);
if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return;
var candy = board[row][col];
if (!candy || candy.blocker !== BLOCKER_NONE) return;
if (!selectedCandy) {
selectedCandy = candy;
candy.setSelected(true);
} else if (selectedCandy === candy) {
deselectAll();
} else if (areAdjacent(selectedCandy, candy)) {
// Swap
selectedCandy.setSelected(false);
candy.setSelected(false);
swapCandies(selectedCandy, candy, function () {
// Check for matches
var matches = findMatches();
if (matches.length > 0) {
processMatches();
} else {
// No match, swap back
LK.getSound('fail').play();
swapCandies(selectedCandy, candy, function () {
swapping = false;
deselectAll();
});
}
movesLeft--;
updateGUI();
});
deselectAll();
} else {
deselectAll();
selectedCandy = candy;
candy.setSelected(true);
}
}
// Process matches and refill
function processMatches() {
isProcessing = true;
var matches = findMatches();
if (matches.length === 0) {
swapping = false;
isProcessing = false;
checkGameEnd();
return;
}
removeMatches(matches, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
}
// Update GUI
function updateGUI() {
scoreTxt.setText('Score: ' + score);
movesTxt.setText('Moves: ' + movesLeft);
targetTxt.setText('Target: ' + targetScore);
}
// Check for win/lose
function checkGameEnd() {
if (score >= targetScore) {
LK.showYouWin();
} else if (movesLeft <= 0) {
LK.showGameOver();
}
}
// Game event handlers
game.down = function (x, y, obj) {
// Don't allow tap in top left 100x100
if (x < 100 && y < 100) return;
handleTap(x, y, obj);
};
// No drag/move for now
// Game update
game.update = function () {
// No per-frame logic needed for MVP
};
// Start music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.7,
duration: 1000
}
});
// Start game
initBoard();
updateGUI(); ===================================================================
--- original.js
+++ change.js
@@ -1,6 +1,518 @@
-/****
+/****
+* Plugins
+****/
+var tween = LK.import("@upit/tween.v1");
+
+/****
+* Classes
+****/
+// Candy class
+var Candy = Container.expand(function () {
+ var self = Container.call(this);
+ // Properties
+ self.type = getRandomCandyType();
+ self.special = SPECIAL_NONE;
+ self.blocker = BLOCKER_NONE;
+ self.row = 0;
+ self.col = 0;
+ self.isFalling = false;
+ self.isMatched = false;
+ self.isSelected = false;
+ self.asset = null;
+ // Attach asset
+ function updateAsset() {
+ if (self.asset) {
+ self.removeChild(self.asset);
+ self.asset.destroy();
+ }
+ var assetId = self.type;
+ if (self.special === SPECIAL_STRIPED) assetId = 'candy_striped';
+ if (self.special === SPECIAL_BOMB) assetId = 'candy_bomb';
+ if (self.special === SPECIAL_RAINBOW) assetId = 'candy_rainbow';
+ if (self.blocker === BLOCKER_CHOCOLATE) assetId = 'blocker_chocolate';
+ if (self.blocker === BLOCKER_ICE) assetId = 'blocker_ice';
+ self.asset = self.attachAsset(assetId, {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ if (self.isSelected) {
+ self.asset.scaleX = 1.15;
+ self.asset.scaleY = 1.15;
+ } else {
+ self.asset.scaleX = 1;
+ self.asset.scaleY = 1;
+ }
+ }
+ // Set type
+ self.setType = function (type, special, blocker) {
+ self.type = type || getRandomCandyType();
+ self.special = special || SPECIAL_NONE;
+ self.blocker = blocker || BLOCKER_NONE;
+ updateAsset();
+ };
+ // Set selected
+ self.setSelected = function (selected) {
+ self.isSelected = selected;
+ updateAsset();
+ };
+ // Animate to position
+ self.moveTo = function (x, y, duration, onFinish) {
+ tween(self, {
+ x: x,
+ y: y
+ }, {
+ duration: duration || 200,
+ easing: tween.easeInOut,
+ onFinish: onFinish
+ });
+ };
+ // Destroy
+ self.destroyCandy = function () {
+ if (self.asset) {
+ self.removeChild(self.asset);
+ self.asset.destroy();
+ self.asset = null;
+ }
+ self.destroy();
+ };
+ // Init
+ updateAsset();
+ return self;
+});
+
+/****
* Initialize Game
-****/
+****/
var game = new LK.Game({
- backgroundColor: 0x000000
-});
\ No newline at end of file
+ backgroundColor: 0x222244
+});
+
+/****
+* Game Code
+****/
+// Candy types
+// Music
+// Sounds
+// Blockers
+// Special candies
+// Candy shapes/colors
+// Board data
+var CANDY_TYPES = ['candy_red', 'candy_green', 'candy_blue', 'candy_yellow', 'candy_purple', 'candy_orange'];
+// Special types
+var SPECIAL_NONE = 0;
+var SPECIAL_STRIPED = 1;
+var SPECIAL_BOMB = 2;
+var SPECIAL_RAINBOW = 3;
+// Blocker types
+var BLOCKER_NONE = 0;
+var BLOCKER_CHOCOLATE = 1;
+var BLOCKER_ICE = 2;
+// Board size
+var BOARD_COLS = 7;
+var BOARD_ROWS = 9;
+var CELL_SIZE = 200;
+var BOARD_OFFSET_X = Math.floor((2048 - BOARD_COLS * CELL_SIZE) / 2);
+var BOARD_OFFSET_Y = 300;
+// Helper: get random candy type
+function getRandomCandyType() {
+ return CANDY_TYPES[Math.floor(Math.random() * CANDY_TYPES.length)];
+}
+// Helper: get random int
+function randInt(a, b) {
+ return a + Math.floor(Math.random() * (b - a + 1));
+}
+var board = [];
+var candies = [];
+var selectedCandy = null;
+var swapping = false;
+var animating = false;
+var movesLeft = 20;
+var targetScore = 5000;
+var score = 0;
+var scoreTxt = null;
+var movesTxt = null;
+var targetTxt = null;
+var boardContainer = null;
+var matchQueue = [];
+var refillQueue = [];
+var isProcessing = false;
+// GUI
+scoreTxt = new Text2('Score: 0', {
+ size: 90,
+ fill: "#fff"
+});
+scoreTxt.anchor.set(0.5, 0);
+LK.gui.top.addChild(scoreTxt);
+movesTxt = new Text2('Moves: 20', {
+ size: 70,
+ fill: "#fff"
+});
+movesTxt.anchor.set(0.5, 0);
+LK.gui.top.addChild(movesTxt);
+movesTxt.y = 110;
+targetTxt = new Text2('Target: 5000', {
+ size: 60,
+ fill: "#fff"
+});
+targetTxt.anchor.set(0.5, 0);
+LK.gui.top.addChild(targetTxt);
+targetTxt.y = 180;
+// Board container
+boardContainer = new Container();
+game.addChild(boardContainer);
+boardContainer.x = BOARD_OFFSET_X;
+boardContainer.y = BOARD_OFFSET_Y;
+// Initialize board
+function initBoard() {
+ // Clear previous
+ for (var i = 0; i < candies.length; ++i) {
+ if (candies[i]) candies[i].destroyCandy();
+ }
+ candies = [];
+ board = [];
+ for (var row = 0; row < BOARD_ROWS; ++row) {
+ board[row] = [];
+ for (var col = 0; col < BOARD_COLS; ++col) {
+ var candy = new Candy();
+ candy.row = row;
+ candy.col = col;
+ candy.x = col * CELL_SIZE + CELL_SIZE / 2;
+ candy.y = row * CELL_SIZE + CELL_SIZE / 2;
+ boardContainer.addChild(candy);
+ candies.push(candy);
+ board[row][col] = candy;
+ }
+ }
+ // Remove initial matches
+ removeInitialMatches();
+}
+// Remove initial matches to avoid auto-matches at start
+function removeInitialMatches() {
+ for (var row = 0; row < BOARD_ROWS; ++row) {
+ for (var col = 0; col < BOARD_COLS; ++col) {
+ var type = board[row][col].type;
+ // Check left
+ if (col >= 2 && board[row][col - 1].type === type && board[row][col - 2].type === type) {
+ var newType = getRandomCandyType();
+ while (newType === type) newType = getRandomCandyType();
+ board[row][col].setType(newType);
+ }
+ // Check up
+ if (row >= 2 && board[row - 1][col].type === type && board[row - 2][col].type === type) {
+ var newType = getRandomCandyType();
+ while (newType === type) newType = getRandomCandyType();
+ board[row][col].setType(newType);
+ }
+ }
+ }
+}
+// Get candy at board position
+function getCandyAt(row, col) {
+ if (row < 0 || row >= BOARD_ROWS || col < 0 || col >= BOARD_COLS) return null;
+ return board[row][col];
+}
+// Swap two candies
+function swapCandies(c1, c2, cb) {
+ swapping = true;
+ var r1 = c1.row,
+ c1c = c1.col,
+ r2 = c2.row,
+ c2c = c2.col;
+ // Swap in board
+ board[r1][c1c] = c2;
+ board[r2][c2c] = c1;
+ // Swap row/col
+ var tmpRow = c1.row,
+ tmpCol = c1.col;
+ c1.row = c2.row;
+ c1.col = c2.col;
+ c2.row = tmpRow;
+ c2.col = tmpCol;
+ // Animate
+ var done = 0;
+ c1.moveTo(c1.col * CELL_SIZE + CELL_SIZE / 2, c1.row * CELL_SIZE + CELL_SIZE / 2, 180, function () {
+ done++;
+ if (done === 2 && cb) cb();
+ });
+ c2.moveTo(c2.col * CELL_SIZE + CELL_SIZE / 2, c2.row * CELL_SIZE + CELL_SIZE / 2, 180, function () {
+ done++;
+ if (done === 2 && cb) cb();
+ });
+ LK.getSound('swap').play();
+}
+// Check if two candies are adjacent
+function areAdjacent(c1, c2) {
+ var dr = Math.abs(c1.row - c2.row);
+ var dc = Math.abs(c1.col - c2.col);
+ return dr + dc === 1;
+}
+// Find all matches on the board
+function findMatches() {
+ var matches = [];
+ // Horizontal
+ for (var row = 0; row < BOARD_ROWS; ++row) {
+ var count = 1;
+ for (var col = 1; col < BOARD_COLS; ++col) {
+ var prev = board[row][col - 1];
+ var curr = board[row][col];
+ if (curr.type === prev.type && curr.blocker === BLOCKER_NONE && prev.blocker === BLOCKER_NONE) {
+ count++;
+ } else {
+ if (count >= 3) {
+ for (var k = 0; k < count; ++k) {
+ matches.push(board[row][col - 1 - k]);
+ }
+ }
+ count = 1;
+ }
+ }
+ if (count >= 3) {
+ for (var k = 0; k < count; ++k) {
+ matches.push(board[row][BOARD_COLS - 1 - k]);
+ }
+ }
+ }
+ // Vertical
+ for (var col = 0; col < BOARD_COLS; ++col) {
+ var count = 1;
+ for (var row = 1; row < BOARD_ROWS; ++row) {
+ var prev = board[row - 1][col];
+ var curr = board[row][col];
+ if (curr.type === prev.type && curr.blocker === BLOCKER_NONE && prev.blocker === BLOCKER_NONE) {
+ count++;
+ } else {
+ if (count >= 3) {
+ for (var k = 0; k < count; ++k) {
+ matches.push(board[row - 1 - k][col]);
+ }
+ }
+ count = 1;
+ }
+ }
+ if (count >= 3) {
+ for (var k = 0; k < count; ++k) {
+ matches.push(board[BOARD_ROWS - 1 - k][col]);
+ }
+ }
+ }
+ // Remove duplicates
+ var unique = [];
+ for (var i = 0; i < matches.length; ++i) {
+ if (unique.indexOf(matches[i]) === -1) unique.push(matches[i]);
+ }
+ return unique;
+}
+// Remove matched candies and animate
+function removeMatches(matches, cb) {
+ if (!matches || matches.length === 0) {
+ if (cb) cb();
+ return;
+ }
+ LK.getSound('match').play();
+ var done = 0;
+ for (var i = 0; i < matches.length; ++i) {
+ var candy = matches[i];
+ candy.isMatched = true;
+ // Animate scale down and fade
+ tween(candy, {
+ scaleX: 0,
+ scaleY: 0,
+ alpha: 0
+ }, {
+ duration: 200,
+ easing: tween.easeIn,
+ onFinish: function (candy) {
+ return function () {
+ if (candy.asset) {
+ candy.removeChild(candy.asset);
+ candy.asset.destroy();
+ candy.asset = null;
+ }
+ candy.visible = false;
+ done++;
+ if (done === matches.length && cb) cb();
+ };
+ }(candy)
+ });
+ // Add score
+ score += 100;
+ }
+}
+// Drop candies to fill empty spaces
+function dropCandies(cb) {
+ var moved = false;
+ for (var col = 0; col < BOARD_COLS; ++col) {
+ for (var row = BOARD_ROWS - 1; row >= 0; --row) {
+ var candy = board[row][col];
+ if (!candy.isMatched && candy.visible) continue;
+ // Find nearest above
+ for (var above = row - 1; above >= 0; --above) {
+ var aboveCandy = board[above][col];
+ if (!aboveCandy.isMatched && aboveCandy.visible) {
+ // Move aboveCandy down
+ board[row][col] = aboveCandy;
+ aboveCandy.row = row;
+ aboveCandy.col = col;
+ aboveCandy.moveTo(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 180);
+ board[above][col] = candy;
+ moved = true;
+ break;
+ }
+ }
+ }
+ }
+ if (cb) LK.setTimeout(cb, moved ? 200 : 0);
+}
+// Fill empty spaces with new candies
+function refillBoard(cb) {
+ var created = false;
+ for (var col = 0; col < BOARD_COLS; ++col) {
+ for (var row = 0; row < BOARD_ROWS; ++row) {
+ var candy = board[row][col];
+ if (!candy.isMatched && candy.visible) continue;
+ // Create new candy
+ var newCandy = new Candy();
+ newCandy.row = row;
+ newCandy.col = col;
+ newCandy.x = col * CELL_SIZE + CELL_SIZE / 2;
+ newCandy.y = -CELL_SIZE + CELL_SIZE / 2;
+ boardContainer.addChild(newCandy);
+ candies.push(newCandy);
+ board[row][col] = newCandy;
+ newCandy.moveTo(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 220);
+ created = true;
+ }
+ }
+ if (cb) LK.setTimeout(cb, created ? 220 : 0);
+}
+// Remove matched candies from board
+function clearMatchedCandies() {
+ for (var row = 0; row < BOARD_ROWS; ++row) {
+ for (var col = 0; col < BOARD_COLS; ++col) {
+ var candy = board[row][col];
+ if (candy.isMatched) {
+ candy.destroyCandy();
+ // Remove from candies array
+ for (var i = 0; i < candies.length; ++i) {
+ if (candies[i] === candy) {
+ candies.splice(i, 1);
+ break;
+ }
+ }
+ // Replace with dummy invisible candy for drop logic
+ var dummy = new Candy();
+ dummy.row = row;
+ dummy.col = col;
+ dummy.visible = false;
+ board[row][col] = dummy;
+ }
+ }
+ }
+}
+// Deselect all candies
+function deselectAll() {
+ for (var i = 0; i < candies.length; ++i) {
+ candies[i].setSelected(false);
+ }
+ selectedCandy = null;
+}
+// Handle user tap
+function handleTap(x, y, obj) {
+ if (swapping || animating || isProcessing) return;
+ // Convert to board coordinates
+ var local = boardContainer.toLocal({
+ x: x,
+ y: y
+ });
+ var col = Math.floor(local.x / CELL_SIZE);
+ var row = Math.floor(local.y / CELL_SIZE);
+ if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return;
+ var candy = board[row][col];
+ if (!candy || candy.blocker !== BLOCKER_NONE) return;
+ if (!selectedCandy) {
+ selectedCandy = candy;
+ candy.setSelected(true);
+ } else if (selectedCandy === candy) {
+ deselectAll();
+ } else if (areAdjacent(selectedCandy, candy)) {
+ // Swap
+ selectedCandy.setSelected(false);
+ candy.setSelected(false);
+ swapCandies(selectedCandy, candy, function () {
+ // Check for matches
+ var matches = findMatches();
+ if (matches.length > 0) {
+ processMatches();
+ } else {
+ // No match, swap back
+ LK.getSound('fail').play();
+ swapCandies(selectedCandy, candy, function () {
+ swapping = false;
+ deselectAll();
+ });
+ }
+ movesLeft--;
+ updateGUI();
+ });
+ deselectAll();
+ } else {
+ deselectAll();
+ selectedCandy = candy;
+ candy.setSelected(true);
+ }
+}
+// Process matches and refill
+function processMatches() {
+ isProcessing = true;
+ var matches = findMatches();
+ if (matches.length === 0) {
+ swapping = false;
+ isProcessing = false;
+ checkGameEnd();
+ return;
+ }
+ removeMatches(matches, function () {
+ clearMatchedCandies();
+ dropCandies(function () {
+ refillBoard(function () {
+ processMatches();
+ });
+ });
+ });
+}
+// Update GUI
+function updateGUI() {
+ scoreTxt.setText('Score: ' + score);
+ movesTxt.setText('Moves: ' + movesLeft);
+ targetTxt.setText('Target: ' + targetScore);
+}
+// Check for win/lose
+function checkGameEnd() {
+ if (score >= targetScore) {
+ LK.showYouWin();
+ } else if (movesLeft <= 0) {
+ LK.showGameOver();
+ }
+}
+// Game event handlers
+game.down = function (x, y, obj) {
+ // Don't allow tap in top left 100x100
+ if (x < 100 && y < 100) return;
+ handleTap(x, y, obj);
+};
+// No drag/move for now
+// Game update
+game.update = function () {
+ // No per-frame logic needed for MVP
+};
+// Start music
+LK.playMusic('bgmusic', {
+ fade: {
+ start: 0,
+ end: 0.7,
+ duration: 1000
+ }
+});
+// Start game
+initBoard();
+updateGUI();
\ No newline at end of file