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 = 180; // smaller so assets have a normal gap between them and do not overlap 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 = 230; // slightly smaller so assets are adjacent but do not overlap
+var CELL_SIZE = 180; // smaller so assets have a normal gap between them and do not overlap
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() {