User prompt
When assets are moved, they get stuck on top of each other. Solve this problem
User prompt
Fix bugs
User prompt
Sections must have definitive solutions
User prompt
Show the player's level in the top right corner.
User prompt
Every level should has a solutions
User prompt
Split into 3 lines for readability
User prompt
The description section should be 3 lines below each other.
User prompt
Adjust the sizes of all assets to the most appropriate values.
User prompt
Fix bugs
User prompt
Reset size
User prompt
Revert to the old state, cancel all changes regarding asset sizes
User prompt
Let the asset sizes be a little smaller and let there be a normal gap between them.
User prompt
A little smaller so they don't overlap
User prompt
Assets should be slightly larger and adjacent to each other
User prompt
The game should be bigger and fit the screen. The background should be more enjoyable. The background should change at each level.
User prompt
The description section should be written in 3 lines below each other.
User prompt
Let the description section fit on the phone screen
User prompt
When the game starts, the description section will slide from bottom to top on the screen, read it and disappear. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
The description section should be clear and visible and should fit on a slightly larger screen.
User prompt
Bug There is a bug. There are overlapping frames.
User prompt
Please fix the bug: 'Timeout.tick error: storage.set is not a function' in or related to this line: 'storage.set('currentLevel', currentLevel);' Line Number: 1382 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'storage.get is not a function' in or related to this line: 'var currentLevel = storage.get('currentLevel') !== undefined ? storage.get('currentLevel') : 0;' Line Number: 500 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Add new features to the game
User prompt
Next level Let the option be a button and it can be determined by assets.
User prompt
Let the next level text at the end of the section be like a button and I will shape it with assets.
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.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
****/
// Board data
// Candy shapes/colors
// Special candies
// Blockers
// Sounds
// Music
// Candy types
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 (BIGGER, fits screen better)
var BOARD_COLS = 7;
var BOARD_ROWS = 9;
var CELL_SIZE = 250; // match asset size so candies are adjacent
var BOARD_OFFSET_X = Math.floor((2048 - BOARD_COLS * CELL_SIZE) / 2);
var BOARD_OFFSET_Y = 120; // move up, fits on tall screens
// 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;
// Level system
// Level design: early levels are easy, later levels are harder and require more skill
// Unique level designs for variety and gameplay changes
var levels = [
// Level 1: Classic match-3, no blockers, easy intro
{
moves: 20,
target: 400,
blockers: [],
description: "Classic: No blockers, just match candies!"
},
// Level 2: Chocolate row in the middle, must clear to progress
{
moves: 18,
target: 600,
blockers: [{
row: 4,
type: BLOCKER_CHOCOLATE
}],
description: "Chocolate row blocks the center. Clear it!"
},
// Level 3: Ice on the bottom, candies drop slower (simulate by more blockers)
{
moves: 16,
target: 800,
blockers: [{
row: 7,
type: BLOCKER_ICE
}, {
row: 8,
type: BLOCKER_ICE
}],
description: "Frozen bottom! Break the ice to win."
},
// Level 4: Alternating chocolate and ice, zig-zag pattern
{
moves: 15,
target: 1000,
blockers: [{
row: 2,
type: BLOCKER_CHOCOLATE
}, {
row: 3,
type: BLOCKER_ICE
}, {
row: 4,
type: BLOCKER_CHOCOLATE
}, {
row: 5,
type: BLOCKER_ICE
}],
description: "Zig-zag blockers! Plan your matches."
},
// Level 5: Boss - chocolate and ice checkerboard, higher target
{
moves: 14,
target: 1400,
blockers: [{
row: 2,
type: BLOCKER_CHOCOLATE
}, {
row: 3,
type: BLOCKER_ICE
}, {
row: 4,
type: BLOCKER_CHOCOLATE
}, {
row: 5,
type: BLOCKER_ICE
}, {
row: 6,
type: BLOCKER_CHOCOLATE
}],
description: "Boss: Checkerboard blockers! Big challenge."
},
// Level 6: Cross and diamond of ice, with chocolate corners
{
moves: 13,
target: 1700,
blockers: [{
row: 0,
type: BLOCKER_CHOCOLATE
}, {
row: 4,
type: BLOCKER_ICE
}, {
row: 8,
type: BLOCKER_CHOCOLATE
}],
description: "Cross & diamond of ice, chocolate corners. Break through!"
},
// Level 7: Chocolate on the sides, ice in the center, U-shape at bottom
{
moves: 12,
target: 1900,
blockers: [{
row: 0,
type: BLOCKER_CHOCOLATE
}, {
row: 8,
type: BLOCKER_CHOCOLATE
}, {
row: 4,
type: BLOCKER_ICE
}, {
row: 7,
type: BLOCKER_ICE
}],
description: "Sides blocked by chocolate, center by ice, U-shape at bottom."
},
// Level 8: Alternating rows of blockers, vertical stripe in center, random chocolate
{
moves: 12,
target: 2100,
blockers: [{
row: 1,
type: BLOCKER_ICE
}, {
row: 3,
type: BLOCKER_CHOCOLATE
}, {
row: 5,
type: BLOCKER_ICE
}, {
row: 7,
type: BLOCKER_CHOCOLATE
}],
description: "Alternating blockers, vertical stripe in center, random chocolate."
},
// Level 9: Spiral blockers, spiral arm from bottom right, random ice
{
moves: 11,
target: 2400,
blockers: [{
row: 0,
type: BLOCKER_ICE
}, {
row: 1,
type: BLOCKER_CHOCOLATE
}, {
row: 2,
type: BLOCKER_ICE
}, {
row: 3,
type: BLOCKER_CHOCOLATE
}, {
row: 4,
type: BLOCKER_ICE
}, {
row: 5,
type: BLOCKER_CHOCOLATE
}, {
row: 6,
type: BLOCKER_ICE
}, {
row: 7,
type: BLOCKER_CHOCOLATE
}],
description: "Spiral blockers! Can you break out? Watch for spiral arms."
},
// Level 10: Boss - nearly every row blocked, zig-zag path open, random blockers
{
moves: 10,
target: 3000,
blockers: [{
row: 0,
type: BLOCKER_CHOCOLATE
}, {
row: 1,
type: BLOCKER_ICE
}, {
row: 2,
type: BLOCKER_CHOCOLATE
}, {
row: 3,
type: BLOCKER_ICE
}, {
row: 4,
type: BLOCKER_CHOCOLATE
}, {
row: 5,
type: BLOCKER_ICE
}, {
row: 6,
type: BLOCKER_CHOCOLATE
}, {
row: 7,
type: BLOCKER_ICE
}, {
row: 8,
type: BLOCKER_CHOCOLATE
}],
description: "Boss: Nearly full blockers! Survive the gauntlet, zig-zag path open."
},
// Level 11: Hollow square of chocolate, ice in the center
{
moves: 13,
target: 3500,
blockers: [{
row: 0,
type: BLOCKER_CHOCOLATE
}, {
row: 1,
type: BLOCKER_CHOCOLATE
}, {
row: 2,
type: BLOCKER_CHOCOLATE
}, {
row: 6,
type: BLOCKER_CHOCOLATE
}, {
row: 7,
type: BLOCKER_CHOCOLATE
}, {
row: 8,
type: BLOCKER_CHOCOLATE
}, {
row: 4,
type: BLOCKER_ICE
}],
description: "Hollow chocolate square, ice in the center. Break the fortress!"
},
// Level 12: Diagonal chocolate, anti-diagonal ice
{
moves: 12,
target: 4000,
blockers: [{
row: 0,
type: BLOCKER_CHOCOLATE
}, {
row: 1,
type: BLOCKER_ICE
}, {
row: 2,
type: BLOCKER_CHOCOLATE
}, {
row: 3,
type: BLOCKER_ICE
}, {
row: 4,
type: BLOCKER_CHOCOLATE
}, {
row: 5,
type: BLOCKER_ICE
}, {
row: 6,
type: BLOCKER_CHOCOLATE
}, {
row: 7,
type: BLOCKER_ICE
}, {
row: 8,
type: BLOCKER_CHOCOLATE
}],
description: "Diagonal chocolate, anti-diagonal ice. Diagonal thinking required!"
},
// Level 13: Center cross of chocolate, outer ring of ice
{
moves: 11,
target: 4500,
blockers: [{
row: 0,
type: BLOCKER_ICE
}, {
row: 1,
type: BLOCKER_ICE
}, {
row: 2,
type: BLOCKER_ICE
}, {
row: 3,
type: BLOCKER_ICE
}, {
row: 4,
type: BLOCKER_CHOCOLATE
}, {
row: 5,
type: BLOCKER_ICE
}, {
row: 6,
type: BLOCKER_ICE
}, {
row: 7,
type: BLOCKER_ICE
}, {
row: 8,
type: BLOCKER_ICE
}],
description: "Center cross of chocolate, outer ring of ice. Break the cross!"
},
// Level 14: Random blockers, every other column
{
moves: 10,
target: 5000,
blockers: [{
row: 2,
type: BLOCKER_CHOCOLATE
}, {
row: 4,
type: BLOCKER_ICE
}, {
row: 6,
type: BLOCKER_CHOCOLATE
}],
description: "Random blockers, every other column. Unpredictable fun!"
},
// Level 15: Boss - alternating chocolate and ice, spiral and cross
{
moves: 9,
target: 6000,
blockers: [{
row: 0,
type: BLOCKER_CHOCOLATE
}, {
row: 1,
type: BLOCKER_ICE
}, {
row: 2,
type: BLOCKER_CHOCOLATE
}, {
row: 3,
type: BLOCKER_ICE
}, {
row: 4,
type: BLOCKER_CHOCOLATE
}, {
row: 5,
type: BLOCKER_ICE
}, {
row: 6,
type: BLOCKER_CHOCOLATE
}, {
row: 7,
type: BLOCKER_ICE
}, {
row: 8,
type: BLOCKER_CHOCOLATE
}],
description: "Boss: Alternating blockers, spiral and cross. The ultimate challenge!"
}];
var currentLevel = typeof storage.currentLevel !== "undefined" ? storage.currentLevel : 0;
var movesLeft = typeof storage.movesLeft !== "undefined" ? storage.movesLeft : levels[currentLevel].moves;
var targetScore = levels[currentLevel].target;
var score = typeof storage.score !== "undefined" ? storage.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() {
// Change background color for each level for a more enjoyable and dynamic background
var bgColors = [0x222244,
// deep blue
0x2a3d55,
// blue-gray
0x3b2a55,
// purple
0x2a5544,
// teal
0x55442a,
// brown
0x44552a,
// olive
0x2a5555,
// cyan
0x552a3b,
// magenta
0x2a2a55,
// indigo
0x552a2a,
// red
0x2a552a,
// green
0x55552a,
// yellow
0x2a4455,
// blue-green
0x552a44,
// pink
0x444444 // gray
];
// Pick a color based on currentLevel, cycle if more levels than colors
game.setBackgroundColor(bgColors[currentLevel % bgColors.length]);
// 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;
// Unique blocker logic per level
var blockers = levels[currentLevel].blockers;
var placedBlocker = false;
// Guarantee at least one open path per row for passability
// We'll use a random openCol for each row, but keep it consistent for the row
var openCol = -1;
if (blockers.length > 0) {
// For each row, pick a random open column (or center for symmetry)
openCol = Math.floor(BOARD_COLS / 2);
// For more variety, you could use: openCol = randInt(0, BOARD_COLS - 1);
}
for (var b = 0; b < blockers.length; ++b) {
if (row === blockers[b].row) {
// Level-specific patterns
// Level 5/10: checkerboard, but add a diagonal line of blockers for extra challenge
if ((currentLevel === 4 || currentLevel === 9) && ((row + col) % 2 === 0 || row === col)) {
// Always leave openCol open
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Level 6: cross pattern, but add a diamond shape in the center
else if (currentLevel === 5 && (col === Math.floor(BOARD_COLS / 2) || row === Math.floor(BOARD_ROWS / 2) || Math.abs(col - Math.floor(BOARD_COLS / 2)) === Math.abs(row - Math.floor(BOARD_ROWS / 2)) && Math.abs(col - Math.floor(BOARD_COLS / 2)) <= 2)) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Level 7: chocolate on sides, ice in center, but add blockers in a U shape at the bottom
else if (currentLevel === 6 && ((row === 0 || row === BOARD_ROWS - 1) && blockers[b].type === BLOCKER_CHOCOLATE && (col === 0 || col === BOARD_COLS - 1) || row >= BOARD_ROWS - 3 && (col === 0 || col === BOARD_COLS - 1 || row === BOARD_ROWS - 1 && col > 0 && col < BOARD_COLS - 1))) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Level 8: alternating rows, but add a vertical stripe in the center
else if (currentLevel === 7 && (row % 2 === b % 2 || col === Math.floor(BOARD_COLS / 2))) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Level 9: spiral (simulate with increasing rows), but add a spiral arm from the bottom right
else if (currentLevel === 8 && (col >= row && col < BOARD_COLS - row || row + col === BOARD_COLS + BOARD_ROWS - 2 - row && row > BOARD_ROWS / 2)) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Level 10: nearly every row blocked, but leave a zig-zag path open
else if (currentLevel === 9 && !((row + col) % 2 === 1 && col !== 0 && col !== BOARD_COLS - 1)) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Default: alternate blockers on even/odd columns for variety, but add a random chance for double blockers
else if (!placedBlocker && (blockers[b].type === BLOCKER_CHOCOLATE && col % 2 === 0 || blockers[b].type === BLOCKER_ICE && col % 2 === 1 || Math.random() < 0.07)) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
}
}
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 = r2;
c1.col = c2c;
c2.row = r1;
c2.col = c1c;
// Ensure board and candies are in sync after swap
board[c1.row][c1.col] = c1;
board[c2.row][c2.col] = c2;
// 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) {
swapping = false;
cb();
}
});
c2.moveTo(c2.col * CELL_SIZE + CELL_SIZE / 2, c2.row * CELL_SIZE + CELL_SIZE / 2, 180, function () {
done++;
if (done === 2 && cb) {
swapping = false;
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);
// Allow swapping in all four directions (up, down, left, right)
return dr === 1 && dc === 0 || dr === 0 && dc === 1;
}
// Find all matches on the board, including special candy creation
function findMatches() {
var matches = [];
var matchGroups = []; // For special candy creation
// Horizontal
for (var row = 0; row < BOARD_ROWS; ++row) {
var count = 1;
var startCol = 0;
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) {
var group = [];
for (var k = 0; k < count; ++k) {
group.push(board[row][col - 1 - k]);
}
matches = matches.concat(group);
matchGroups.push(group);
}
count = 1;
startCol = col;
}
}
if (count >= 3) {
var group = [];
for (var k = 0; k < count; ++k) {
group.push(board[row][BOARD_COLS - 1 - k]);
}
matches = matches.concat(group);
matchGroups.push(group);
}
}
// Vertical
for (var col = 0; col < BOARD_COLS; ++col) {
var count = 1;
var startRow = 0;
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) {
var group = [];
for (var k = 0; k < count; ++k) {
group.push(board[row - 1 - k][col]);
}
matches = matches.concat(group);
matchGroups.push(group);
}
count = 1;
startRow = row;
}
}
if (count >= 3) {
var group = [];
for (var k = 0; k < count; ++k) {
group.push(board[BOARD_ROWS - 1 - k][col]);
}
matches = matches.concat(group);
matchGroups.push(group);
}
}
// Detect 2x2 square matches and mark for bomb
for (var row = 0; row < BOARD_ROWS - 1; ++row) {
for (var col = 0; col < BOARD_COLS - 1; ++col) {
var c1 = board[row][col];
var c2 = board[row][col + 1];
var c3 = board[row + 1][col];
var c4 = board[row + 1][col + 1];
if (c1.type === c2.type && c1.type === c3.type && c1.type === c4.type && c1.blocker === BLOCKER_NONE && c2.blocker === BLOCKER_NONE && c3.blocker === BLOCKER_NONE && c4.blocker === BLOCKER_NONE) {
// Only add if not already in a match group for this square
var alreadyMatched = false;
for (var mg = 0; mg < matchGroups.length; ++mg) {
var g = matchGroups[mg];
if (g.indexOf(c1) !== -1 && g.indexOf(c2) !== -1 && g.indexOf(c3) !== -1 && g.indexOf(c4) !== -1) {
alreadyMatched = true;
break;
}
}
if (!alreadyMatched) {
// Mark the top-left as bomb, others as normal
c1.special = SPECIAL_BOMB;
c1.setType(c1.type, SPECIAL_BOMB, c1.blocker);
// Remove c1 from matches so it is not destroyed, but destroy the other 3
var group = [c2, c3, c4];
matches = matches.concat(group);
matchGroups.push([c1, c2, c3, c4]);
// Store a flag for c1 to trigger a special 2x2 bomb explosion in removeMatches
c1._pendingSquareBomb = true;
}
}
}
}
// Remove duplicates
var unique = [];
for (var i = 0; i < matches.length; ++i) {
if (unique.indexOf(matches[i]) === -1) unique.push(matches[i]);
}
// Mark special candy creation (striped, bomb, rainbow)
for (var g = 0; g < matchGroups.length; ++g) {
var group = matchGroups[g];
if (group.length === 4) {
// Striped candy: horizontal or vertical
var isHorizontal = group[0].row === group[1].row;
var specialCandy = group[1]; // Place special at second in group
if (specialCandy.special === SPECIAL_NONE) {
specialCandy.special = SPECIAL_STRIPED;
specialCandy.setType(specialCandy.type, SPECIAL_STRIPED, specialCandy.blocker);
// Mark orientation for correct effect
specialCandy._stripedOrientation = isHorizontal ? "horizontal" : "vertical";
}
// Remove the special candy from the group so it is not destroyed
for (var i = 0; i < group.length; ++i) {
if (group[i] === specialCandy) {
group.splice(i, 1);
break;
}
}
}
if (group.length === 5) {
// Color bomb (rainbow) only for exactly 5-in-a-row
var specialCandy = group[Math.floor(group.length / 2)]; // Place in the middle
if (specialCandy.special === SPECIAL_NONE) {
specialCandy.special = SPECIAL_RAINBOW;
specialCandy.setType(specialCandy.type, SPECIAL_RAINBOW, specialCandy.blocker);
}
// Remove the special candy from the group so it is not destroyed
for (var i = 0; i < group.length; ++i) {
if (group[i] === specialCandy) {
group.splice(i, 1);
break;
}
}
}
}
// Bomb candy for T or L shape
// Find intersections of horizontal and vertical matches
for (var i = 0; i < matchGroups.length; ++i) {
var groupA = matchGroups[i];
if (groupA.length !== 3) continue;
for (var j = i + 1; j < matchGroups.length; ++j) {
var groupB = matchGroups[j];
if (groupB.length !== 3) continue;
// Check for intersection
for (var a = 0; a < 3; ++a) {
for (var b = 0; b < 3; ++b) {
if (groupA[a] === groupB[b]) {
// Place bomb at intersection
var bombCandy = groupA[a];
if (bombCandy.special === SPECIAL_NONE) {
bombCandy.special = SPECIAL_BOMB;
bombCandy.setType(bombCandy.type, SPECIAL_BOMB, bombCandy.blocker);
}
}
}
}
}
}
return unique;
}
// Remove matched candies and animate, including special candy activation and blockers
function removeMatches(matches, cb) {
if (!matches || matches.length === 0) {
if (cb) cb();
return;
}
LK.getSound('match').play();
var done = 0;
var toRemove = [];
// Activate special candies in matches
for (var i = 0; i < matches.length; ++i) {
var candy = matches[i];
if (candy.special === SPECIAL_STRIPED) {
// Striped candy: clear row if created from horizontal match, column if from vertical
// Determine orientation by checking if the special was created from a horizontal or vertical match
// We'll use a property set during match detection, or fallback to random if not set (legacy)
var isHorizontal = false;
if (typeof candy._stripedOrientation !== "undefined") {
isHorizontal = candy._stripedOrientation === "horizontal";
} else {
// Fallback: random (legacy, but should not happen with new match logic)
isHorizontal = Math.random() < 0.5;
}
if (isHorizontal) {
// Clear row
for (var c = 0; c < BOARD_COLS; ++c) {
var target = board[candy.row][c];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
} else {
// Clear column
for (var r = 0; r < BOARD_ROWS; ++r) {
var target = board[r][candy.col];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
} else if (candy.special === SPECIAL_RAINBOW) {
// Clear all candies of a random type on board
var colorType = getRandomCandyType();
for (var r = 0; r < BOARD_ROWS; ++r) {
for (var c = 0; c < BOARD_COLS; ++c) {
var target = board[r][c];
if (target.type === colorType && toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
} else if (candy.special === SPECIAL_BOMB) {
// If this bomb was created by a 2x2 square match, do a shine and destroy 8 neighbors
if (candy._pendingSquareBomb) {
// Shine effect: flash the bomb candy
LK.effects.flashObject(candy, 0xffff00, 400);
// Destroy 8 neighbors (not self)
for (var dr = -1; dr <= 1; ++dr) {
for (var dc = -1; dc <= 1; ++dc) {
var rr = candy.row + dr,
cc = candy.col + dc;
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
if (rr === candy.row && cc === candy.col) continue; // skip self
var target = board[rr][cc];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
}
// Remove the flag so it doesn't trigger again
delete candy._pendingSquareBomb;
} else {
// Default bomb: Clear 3x3 area (including self)
for (var dr = -1; dr <= 1; ++dr) {
for (var dc = -1; dc <= 1; ++dc) {
var rr = candy.row + dr,
cc = candy.col + dc;
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
var target = board[rr][cc];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
}
}
} else {
if (toRemove.indexOf(candy) === -1) toRemove.push(candy);
}
}
// Remove blockers if matched
for (var i = 0; i < toRemove.length; ++i) {
var candy = toRemove[i];
if (candy.blocker === BLOCKER_CHOCOLATE) {
// Remove chocolate in one match
candy.blocker = BLOCKER_NONE;
candy.setType(candy.type, candy.special, BLOCKER_NONE);
continue;
}
if (candy.blocker === BLOCKER_ICE) {
// Remove ice in two matches: first match cracks, second removes
if (!candy._iceCracked) {
candy._iceCracked = true;
// Tint or visually indicate cracked ice (optional)
candy.setType(candy.type, candy.special, BLOCKER_ICE);
continue;
} else {
candy.blocker = BLOCKER_NONE;
candy.setType(candy.type, candy.special, BLOCKER_NONE);
continue;
}
}
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 === toRemove.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, ensuring correct offset and boundaries
// Remove any offset miscalculations: boardContainer.x/y is already set, so local is relative to grid origin
// For touch/click, always use the event's x/y relative to the game, then subtract boardContainer.x/y to get local grid coordinates
var localX = x - boardContainer.x;
var localY = y - boardContainer.y;
// Clamp localX/localY to be within the grid area
if (localX < 0 || localY < 0) return;
var col = Math.floor(localX / CELL_SIZE);
var row = Math.floor(localY / CELL_SIZE);
// Clamp to grid bounds
// Boundary check
if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return;
var candy = board[row][col];
// Debug: Draw a highlight rectangle over the selected cell
if (typeof handleTap._debugRect !== "undefined" && handleTap._debugRect) {
boardContainer.removeChild(handleTap._debugRect);
handleTap._debugRect.destroy();
handleTap._debugRect = null;
}
var debugRect = LK.getAsset('blocker_ice', {
anchorX: 0,
anchorY: 0,
x: col * CELL_SIZE,
y: row * CELL_SIZE,
width: CELL_SIZE,
height: CELL_SIZE,
alpha: 0.25
});
// Insert debugRect at the bottom of boardContainer's children so it doesn't block candy input
if (boardContainer.children && boardContainer.children.length > 0) {
boardContainer.addChildAt(debugRect, 0);
} else {
boardContainer.addChild(debugRect);
}
handleTap._debugRect = debugRect;
if (!candy || candy.blocker !== BLOCKER_NONE) return;
// If nothing selected, select this candy
if (!selectedCandy) {
deselectAll();
selectedCandy = candy;
candy.setSelected(true);
return;
}
// If clicking the same candy, deselect
if (selectedCandy === candy) {
deselectAll();
return;
}
// If adjacent, try to swap
if (areAdjacent(selectedCandy, candy)) {
swapping = true;
selectedCandy.setSelected(false);
candy.setSelected(false);
// Lock input during animation
var c1 = selectedCandy;
var c2 = candy;
deselectAll();
swapCandies(c1, c2, function () {
// Special candy swap logic
var specialActivated = false;
// Rainbow (color bomb) swap
if (c1.special === SPECIAL_RAINBOW || c2.special === SPECIAL_RAINBOW) {
var colorType = c1.special === SPECIAL_RAINBOW ? c2.type : c1.type;
var toRemove = [];
for (var r = 0; r < BOARD_ROWS; ++r) {
for (var c = 0; c < BOARD_COLS; ++c) {
var target = board[r][c];
if (target.type === colorType) toRemove.push(target);
}
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
updateGUI();
// Animate the level description section to slide up from bottom, pause, then disappear at game start
if (window.levelDescTxt && window.levelDescBg) {
// Start off-screen (y = 200, below visible area)
window.levelDescTxt.y = 200;
window.levelDescBg.y = 200;
// Make sure both are visible
window.levelDescTxt.visible = true;
window.levelDescBg.visible = true;
// Slide up to visible position (-220) over 700ms
tween(window.levelDescTxt, {
y: -220
}, {
duration: 700,
easing: tween.cubicOut
});
tween(window.levelDescBg, {
y: -220
}, {
duration: 700,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Pause for 2.2s, then slide out and hide
LK.setTimeout(function () {
tween(window.levelDescTxt, {
y: -600,
alpha: 0
}, {
duration: 500,
easing: tween.cubicIn,
onFinish: function onFinish() {
window.levelDescTxt.visible = false;
window.levelDescTxt.alpha = 1;
window.levelDescTxt.y = -220;
}
});
tween(window.levelDescBg, {
y: -600,
alpha: 0
}, {
duration: 500,
easing: tween.cubicIn,
onFinish: function onFinish() {
window.levelDescBg.visible = false;
window.levelDescBg.alpha = 0.18;
window.levelDescBg.y = -220;
}
});
}, 2200);
}
});
}
swapping = false;
specialActivated = true;
}
// Striped + striped: clear row and col
else if (c1.special === SPECIAL_STRIPED && c2.special === SPECIAL_STRIPED) {
var toRemove = [];
for (var c = 0; c < BOARD_COLS; ++c) {
var t1 = board[c1.row][c];
if (toRemove.indexOf(t1) === -1) toRemove.push(t1);
}
for (var r = 0; r < BOARD_ROWS; ++r) {
var t2 = board[r][c2.col];
if (toRemove.indexOf(t2) === -1) toRemove.push(t2);
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
updateGUI();
swapping = false;
specialActivated = true;
}
// Bomb + any: clear 3x3 around both
else if (c1.special === SPECIAL_BOMB || c2.special === SPECIAL_BOMB) {
var toRemove = [];
var bombCandies = [c1, c2];
for (var b = 0; b < 2; ++b) {
if (bombCandies[b].special === SPECIAL_BOMB) {
for (var dr = -1; dr <= 1; ++dr) {
for (var dc = -1; dc <= 1; ++dc) {
var rr = bombCandies[b].row + dr,
cc = bombCandies[b].col + dc;
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
var target = board[rr][cc];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
}
}
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
updateGUI();
swapping = false;
specialActivated = true;
}
if (!specialActivated) {
// After swap, check for matches
var matches = findMatches();
if (matches.length > 0) {
processMatches();
movesLeft--;
updateGUI();
swapping = false;
} else {
// No match, swap back
LK.getSound('fail').play();
// Animate a quick shake for both candies
var origX1 = c1.col * CELL_SIZE + CELL_SIZE / 2;
var origY1 = c1.row * CELL_SIZE + CELL_SIZE / 2;
var origX2 = c2.col * CELL_SIZE + CELL_SIZE / 2;
var origY2 = c2.row * CELL_SIZE + CELL_SIZE / 2;
tween(c1, {
x: origX1 + 20
}, {
duration: 60,
onFinish: function onFinish() {
tween(c1, {
x: origX1
}, {
duration: 60
});
}
});
tween(c2, {
x: origX2 - 20
}, {
duration: 60,
onFinish: function onFinish() {
tween(c2, {
x: origX2
}, {
duration: 60
});
}
});
// Actually swap back the candies to their original positions in the board and update their row/col
swapCandies(c1, c2, function () {
swapping = false;
deselectAll();
// Re-select the original candy for user feedback
selectedCandy = c1;
c1.setSelected(true);
});
movesLeft--;
updateGUI();
}
}
});
return;
}
// Not adjacent, select new candy
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;
deselectAll();
// Save progress after move
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
checkGameEnd();
return;
}
removeMatches(matches, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
// Chocolate spread: after all moves, if any chocolate exists, spread to adjacent
var chocolateList = [];
for (var row = 0; row < BOARD_ROWS; ++row) {
for (var col = 0; col < BOARD_COLS; ++col) {
var c = board[row][col];
if (c.blocker === BLOCKER_CHOCOLATE) chocolateList.push(c);
}
}
if (chocolateList.length > 0) {
// Try to spread chocolate to adjacent non-blocker, non-special, non-chocolate
for (var i = 0; i < chocolateList.length; ++i) {
var c = chocolateList[i];
var dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]];
for (var d = 0; d < dirs.length; ++d) {
var rr = c.row + dirs[d][0],
cc = c.col + dirs[d][1];
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
var target = board[rr][cc];
if (target.blocker === BLOCKER_NONE && target.special === SPECIAL_NONE) {
target.blocker = BLOCKER_CHOCOLATE;
target.setType(target.type, target.special, BLOCKER_CHOCOLATE);
break;
}
}
}
}
}
// After all refills, check for new matches
var newMatches = findMatches();
if (newMatches.length > 0) {
processMatches();
} else {
swapping = false;
isProcessing = false;
deselectAll();
// Save progress after move
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
checkGameEnd();
}
});
});
});
}
// Update GUI
function updateGUI() {
scoreTxt.setText('Score: ' + score);
movesTxt.setText('Moves: ' + movesLeft);
targetTxt.setText('Target: ' + targetScore);
// Show "Boss" for every 5th level (level 5, 10, 15, ...)
var levelNum = currentLevel + 1;
if (levelNum % 5 === 0) {
levelTxt.setText('Level: ' + levelNum + ' (Boss!)');
} else {
levelTxt.setText('Level: ' + levelNum);
}
// Show level description at the bottom center, styled for clarity and visibility on large screens
if (!window.levelDescTxt) {
// Reduce font size for phone screens, and background height
window.levelDescTxt = new Text2('', {
size: 54,
// was 90, now fits on phone
fill: "#fff",
font: "Impact, 'Arial Black', Tahoma"
});
window.levelDescTxt.anchor.set(0.5, 1);
LK.gui.bottom.addChild(window.levelDescTxt);
window.levelDescTxt.y = -120; // less offset, fits above bottom buttons
// Add a subtle background for readability, smaller height for phone
window.levelDescBg = LK.getAsset('blocker_ice', {
anchorX: 0.5,
anchorY: 1,
x: 0,
y: -120,
width: 900,
// default, will be resized below
height: 90,
// was 160, now fits phone
alpha: 0.18
});
// Insert background behind text
LK.gui.bottom.addChildAt(window.levelDescBg, Math.max(0, LK.gui.bottom.children.indexOf(window.levelDescTxt)));
}
// Split the description into 3 lines (if possible), or pad with empty lines
var desc = levels[currentLevel].description || '';
var descLines = desc.split('\n');
if (descLines.length < 3) {
// Try to split by ". " or ": " or "! " or "."
if (descLines.length === 1) {
var autoSplit = descLines[0].replace(/([.!?])\s+/g, "$1\n").replace(/: /g, ":\n").split('\n');
descLines = autoSplit;
}
}
while (descLines.length < 3) descLines.push('');
descLines = descLines.slice(0, 3);
window.levelDescTxt.setText(descLines.join('\n'));
// Keep background width in sync with text width (defensive: min 400, max 1200 for phone)
if (window.levelDescBg && window.levelDescTxt) {
var txtW = window.levelDescTxt.width + 60;
if (txtW < 400) txtW = 400;
if (txtW > 1200) txtW = 1200;
window.levelDescBg.width = txtW;
window.levelDescBg.x = 0;
}
}
// Check for win/lose
function checkGameEnd() {
if (score >= targetScore) {
// Next level if available, else show win
if (currentLevel < levels.length - 1) {
// Flash the episode score
LK.effects.flashScreen(0xffff00, 800);
// Show a big score text in the center
if (!window.episodeScoreTxt) {
window.episodeScoreTxt = new Text2('', {
size: 180,
fill: "#fff"
});
window.episodeScoreTxt.anchor.set(0.5, 0.5);
LK.gui.center.addChild(window.episodeScoreTxt);
}
window.episodeScoreTxt.setText('Score: ' + score);
window.episodeScoreTxt.visible = true;
// Hide any previous next level frame and score text to prevent overlap
if (window.episodeScoreTxt) window.episodeScoreTxt.visible = false;
if (window.nextLevelBtnBig) window.nextLevelBtnBig.visible = false;
// Show next level button in the center below the score, as a button with asset background and text label
if (!window.nextLevelBtnBig) {
// Create a container for the button
window.nextLevelBtnBig = new Container();
// Add a button background asset (replace 'prism_button' with your asset id)
var btnBg = LK.getAsset('prism_button', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
width: 500,
height: 160
});
window.nextLevelBtnBig.addChild(btnBg);
// Add the text label on top
var btnLabel = new Text2('Next Level', {
size: 120,
fill: 0xFFFFFF,
font: "Impact, 'Arial Black', Tahoma"
});
btnLabel.anchor.set(0.5, 0.5);
btnLabel.x = 0;
btnLabel.y = 0;
window.nextLevelBtnBig.addChild(btnLabel);
window.nextLevelBtnBig.anchorX = 0.5;
window.nextLevelBtnBig.anchorY = 0.5;
window.nextLevelBtnBig.x = 0;
window.nextLevelBtnBig.y = 200;
LK.gui.center.addChild(window.nextLevelBtnBig);
window.nextLevelBtnBig.interactive = true;
window.nextLevelBtnBig.buttonMode = true;
window.nextLevelBtnBig.down = function (x, y, obj) {
if (window.episodeScoreTxt) window.episodeScoreTxt.visible = false;
if (window.nextLevelBtnBig) window.nextLevelBtnBig.visible = false;
currentLevel++;
score = 0;
movesLeft = levels[currentLevel].moves;
targetScore = levels[currentLevel].target;
// Save progress
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
updateGUI();
initBoard();
};
}
window.nextLevelBtnBig.visible = true;
if (window.episodeScoreTxt) window.episodeScoreTxt.visible = true;
// Hide after 2 seconds if user doesn't tap
LK.setTimeout(function () {
if (window.episodeScoreTxt) window.episodeScoreTxt.visible = false;
if (window.nextLevelBtnBig) window.nextLevelBtnBig.visible = false;
if (window.nextLevelBtnBig && window.nextLevelBtnBig.visible) {
// If still visible, auto-advance
window.nextLevelBtnBig.down();
}
}, 4000);
} else {
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);
};
// Drag-to-swap logic
var dragStartCandy = null;
var dragCurrentCandy = null;
var dragActive = false;
// Helper: get candy at pixel position (returns null if not valid)
function getCandyAtPixel(x, y) {
var localX = x - boardContainer.x;
var localY = y - boardContainer.y;
if (localX < 0 || localY < 0) return null;
var col = Math.floor(localX / CELL_SIZE);
var row = Math.floor(localY / CELL_SIZE);
if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return null;
return board[row][col];
}
// Override game.down for drag start
game.down = function (x, y, obj) {
// Don't allow tap in top left 100x100
if (x < 100 && y < 100) return;
if (swapping || animating || isProcessing) return;
var candy = getCandyAtPixel(x, y);
if (!candy || candy.blocker !== BLOCKER_NONE) return;
dragStartCandy = candy;
dragCurrentCandy = candy;
dragActive = true;
deselectAll();
candy.setSelected(true);
selectedCandy = candy;
};
// Drag move handler
game.move = function (x, y, obj) {
if (!dragActive || swapping || animating || isProcessing) return;
var candy = getCandyAtPixel(x, y);
if (!candy || candy.blocker !== BLOCKER_NONE) return;
if (candy === dragStartCandy) return;
// Only allow swap with adjacent
if (areAdjacent(dragStartCandy, candy)) {
// Lock drag
dragActive = false;
dragStartCandy.setSelected(false);
candy.setSelected(false);
var c1 = dragStartCandy;
var c2 = candy;
deselectAll();
swapCandies(c1, c2, function () {
// Special candy swap logic (same as tap)
var specialActivated = false;
if (c1.special === SPECIAL_RAINBOW || c2.special === SPECIAL_RAINBOW) {
var colorType = c1.special === SPECIAL_RAINBOW ? c2.type : c1.type;
var toRemove = [];
for (var r = 0; r < BOARD_ROWS; ++r) {
for (var c = 0; c < BOARD_COLS; ++c) {
var target = board[r][c];
if (target.type === colorType) toRemove.push(target);
}
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
updateGUI();
swapping = false;
specialActivated = true;
} else if (c1.special === SPECIAL_STRIPED && c2.special === SPECIAL_STRIPED) {
var toRemove = [];
for (var c = 0; c < BOARD_COLS; ++c) {
var t1 = board[c1.row][c];
if (toRemove.indexOf(t1) === -1) toRemove.push(t1);
}
for (var r = 0; r < BOARD_ROWS; ++r) {
var t2 = board[r][c2.col];
if (toRemove.indexOf(t2) === -1) toRemove.push(t2);
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
updateGUI();
swapping = false;
specialActivated = true;
} else if (c1.special === SPECIAL_BOMB || c2.special === SPECIAL_BOMB) {
var toRemove = [];
var bombCandies = [c1, c2];
for (var b = 0; b < 2; ++b) {
if (bombCandies[b].special === SPECIAL_BOMB) {
for (var dr = -1; dr <= 1; ++dr) {
for (var dc = -1; dc <= 1; ++dc) {
var rr = bombCandies[b].row + dr,
cc = bombCandies[b].col + dc;
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
var target = board[rr][cc];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
}
}
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
updateGUI();
swapping = false;
specialActivated = true;
}
if (!specialActivated) {
var matches = findMatches();
if (matches.length > 0) {
processMatches();
movesLeft--;
updateGUI();
swapping = false;
} else {
// No match, swap back
LK.getSound('fail').play();
var origX1 = c1.col * CELL_SIZE + CELL_SIZE / 2;
var origY1 = c1.row * CELL_SIZE + CELL_SIZE / 2;
var origX2 = c2.col * CELL_SIZE + CELL_SIZE / 2;
var origY2 = c2.row * CELL_SIZE + CELL_SIZE / 2;
tween(c1, {
x: origX1 + 20
}, {
duration: 60,
onFinish: function onFinish() {
tween(c1, {
x: origX1
}, {
duration: 60
});
}
});
tween(c2, {
x: origX2 - 20
}, {
duration: 60,
onFinish: function onFinish() {
tween(c2, {
x: origX2
}, {
duration: 60
});
}
});
// Actually swap back the candies to their original positions in the board and update their row/col
swapCandies(c1, c2, function () {
swapping = false;
deselectAll();
// Re-select the original candy for user feedback
selectedCandy = c1;
c1.setSelected(true);
});
movesLeft--;
updateGUI();
}
}
});
}
};
// Drag end handler
game.up = function (x, y, obj) {
dragActive = false;
dragStartCandy = null;
dragCurrentCandy = null;
// Deselect all if not swapping
if (!swapping && !animating && !isProcessing) {
deselectAll();
}
};
// 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
}
});
// Level label at bottom left
var levelTxt = new Text2('Level: 1', {
size: 70,
fill: "#fff"
});
levelTxt.anchor.set(0, 1);
LK.gui.bottomLeft.addChild(levelTxt);
levelTxt.x = 0;
levelTxt.y = 0;
// Add previous level button at the bottom left (but not in the top left 100x100 area)
var prevLevelBtn = new Text2('Prev', {
size: 90,
fill: "#fff"
});
prevLevelBtn.anchor.set(0, 1);
LK.gui.bottomLeft.addChild(prevLevelBtn);
prevLevelBtn.x = 120; // avoid top left menu area
prevLevelBtn.y = 0;
prevLevelBtn.interactive = true;
prevLevelBtn.buttonMode = true;
prevLevelBtn.down = function (x, y, obj) {
if (currentLevel > 0) {
currentLevel--;
score = 0;
movesLeft = levels[currentLevel].moves;
targetScore = levels[currentLevel].target;
// Save progress
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
updateGUI();
initBoard();
}
};
// Add reset button at the bottom center
var resetBtn = new Text2('Reset', {
size: 90,
fill: "#fff"
});
resetBtn.anchor.set(0.5, 1);
LK.gui.bottom.addChild(resetBtn);
resetBtn.y = 0; // flush to bottom
resetBtn.interactive = true;
resetBtn.buttonMode = true;
resetBtn.down = function (x, y, obj) {
// Reset current level: reset score, moves, board
score = 0;
movesLeft = levels[currentLevel].moves;
targetScore = levels[currentLevel].target;
// Save progress
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
updateGUI();
initBoard();
};
// Add next level button at the bottom right
var nextLevelBtn = new Text2('Next', {
size: 90,
fill: "#fff"
});
nextLevelBtn.anchor.set(1, 1);
LK.gui.bottomRight.addChild(nextLevelBtn);
nextLevelBtn.x = 0;
nextLevelBtn.y = 0;
nextLevelBtn.interactive = true;
nextLevelBtn.buttonMode = true;
nextLevelBtn.down = function (x, y, obj) {
if (currentLevel < levels.length - 1) {
currentLevel++;
score = 0;
movesLeft = levels[currentLevel].moves;
targetScore = levels[currentLevel].target;
// Save progress
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
updateGUI();
initBoard();
}
};
// Start game
initBoard();
updateGUI(); ===================================================================
--- original.js
+++ change.js
@@ -110,9 +110,9 @@
var BLOCKER_ICE = 2;
// Board size (BIGGER, fits screen better)
var BOARD_COLS = 7;
var BOARD_ROWS = 9;
-var CELL_SIZE = 260; // was 200, now 260 for bigger candies and board
+var CELL_SIZE = 250; // match asset size so candies are adjacent
var BOARD_OFFSET_X = Math.floor((2048 - BOARD_COLS * CELL_SIZE) / 2);
var BOARD_OFFSET_Y = 120; // move up, fits on tall screens
// Helper: get random candy type
function getRandomCandyType() {