/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { level: 1 }); /**** * Classes ****/ // --- BuffCard Class --- var BuffCard = Container.expand(function () { var self = Container.call(this); self.buffId = null; self.buffDesc = ''; self.selected = false; self.bg = self.attachAsset('boardBg', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.1, scaleY: 1.4, alpha: 0.93 }); // Add image asset for the card (will be set in setBuff) self.image = null; // Text for description self.text = new Text2('', { size: 60, fill: 0xffffff, stroke: 0x000000, strokeThickness: 8, wordWrap: true, wordWrapWidth: 320 }); self.text.anchor.set(0.5, 0.5); self.text.y = 120; // Move text down to make space for image self.addChild(self.text); // Map buffId to image asset id (add your own asset ids as needed) var BUFF_IMAGE_MAP = { extraMove: 'candyBlue', scoreBoost: 'candyYellow', comboStart: 'candyRed', easyObjective: 'candyGreen', bombChance: 'candyBomb', jellyClear: 'jelly' }; self.setBuff = function (buffId, desc) { self.buffId = buffId; self.buffDesc = desc; self.text.setText(desc); if (self.text.style) { self.text.style.stroke = 0x000000; self.text.style.strokeThickness = 8; } // Remove previous image if any if (self.image) { self.removeChild(self.image); self.image = null; } // Add image asset for this buff var assetId = BUFF_IMAGE_MAP[buffId]; if (assetId) { self.image = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, y: -40, // Place image above text scaleX: 0.7, scaleY: 0.7 }); } }; self.down = function (x, y, obj) { if (self.selected) return; self.selected = true; if (typeof self.onSelect === "function") self.onSelect(self.buffId); }; return self; }); // Candy class var Candy = Container.expand(function () { var self = Container.call(this); self.candyType = null; // e.g. 'red', 'green', etc. self.special = null; // null, 'stripedH', 'stripedV', 'wrapped', 'bomb' self.row = 0; self.col = 0; self.isMoving = false; self.isMatched = false; self.blocker = false; self.jelly = false; self.asset = null; self.setCandy = function (type, special) { self.candyType = type; self.special = special || null; if (self.asset) { self.removeChild(self.asset); } var assetId = 'candy' + type.charAt(0).toUpperCase() + type.slice(1); if (special === 'stripedH') assetId = 'candyStripedH'; if (special === 'stripedV') assetId = 'candyStripedV'; if (special === 'wrapped') assetId = 'candyWrapped'; if (special === 'bomb') assetId = 'candyBomb'; self.asset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); }; self.showJelly = function () { if (!self.jellyAsset) { self.jellyAsset = self.attachAsset('jelly', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5 }); } }; self.hideJelly = function () { if (self.jellyAsset) { self.removeChild(self.jellyAsset); self.jellyAsset = null; } }; self.showBlocker = function () { if (!self.blockerAsset) { self.blockerAsset = self.attachAsset('blocker', { anchorX: 0.5, anchorY: 0.5, alpha: 0.7 }); } }; self.hideBlocker = function () { if (self.blockerAsset) { self.removeChild(self.blockerAsset); self.blockerAsset = null; } }; return self; }); // --- CandyBomb Class --- var CandyBomb = Container.expand(function () { var self = Container.call(this); self.row = 0; self.col = 0; self.isMoving = false; self.asset = self.attachAsset('candyBomb', { anchorX: 0.5, anchorY: 0.5 }); self.setPosition = function (row, col) { self.row = row; self.col = col; self.x = BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2; self.y = BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2; }; self.updateBoardPos = function () { self.row = Math.round((self.y - BOARD_OFFSET_Y - CELL_SIZE / 2) / CELL_SIZE); self.col = Math.round((self.x - BOARD_OFFSET_X - CELL_SIZE / 2) / CELL_SIZE); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ // No title, no description // Always backgroundColor is black backgroundColor: 0x000000 }); /**** * Game Code ****/ // --- Board Settings --- // Candy shapes/colors // Special candies // Board background // Jelly overlay // Blocker // Sound assets // Combo effect image assets (replace with your own asset ids as needed) var BOARD_ROWS = 9; var BOARD_COLS = 9; var CELL_SIZE = 180; var BOARD_OFFSET_X = (2048 - CELL_SIZE * BOARD_COLS) / 2; var BOARD_OFFSET_Y = 350; var CANDY_TYPES = ['Red', 'Green', 'Blue', 'Yellow', 'Purple', 'Orange']; var MOVE_LIMIT = 25; // --- Game State --- var board = []; var candies = []; var selectedCandy = null; var swappingCandy = null; var canInput = true; var movesLeft = MOVE_LIMIT; var score = 0; var level = storage.level || 1; var objective = { type: 'score', value: 2000 + (level - 1) * 400 // Target increases with level }; // MVP: score target // --- Bomb System --- var bomb = null; var bombDragging = false; // Adjust candy types for higher levels (more types as level increases, max 6) var CANDY_TYPES = ['Red', 'Green', 'Blue', 'Yellow', 'Purple', 'Orange']; function getCandyTypesForLevel(lvl) { var count = 4 + Math.floor((lvl - 1) / 2); if (count > 6) count = 6; return CANDY_TYPES.slice(0, count); } var scoreTxt, movesTxt, levelTxt, objectiveTxt; // --- Combo System --- var comboCount = 0; var comboTimer = null; var comboTxt = new Text2('', { size: 90, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 8 }); comboTxt.anchor.set(0, 0); comboTxt.x = BOARD_OFFSET_X - 180; comboTxt.y = BOARD_OFFSET_Y + 40; game.addChild(comboTxt); // --- Board Container --- // Add background image to game, behind everything var mainBg = LK.getAsset('mainBg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 2732 }); game.addChild(mainBg); var boardContainer = new Container(); game.addChild(boardContainer); // --- Board Background --- var boardBg = LK.getAsset('boardBg', { anchorX: 0, anchorY: 0, x: BOARD_OFFSET_X, y: BOARD_OFFSET_Y, width: CELL_SIZE * BOARD_COLS, height: CELL_SIZE * BOARD_ROWS }); boardContainer.addChildAt(boardBg, 0); // --- GUI --- scoreTxt = new Text2('Score: 0', { size: 80, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 8 }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); movesTxt = new Text2('Moves: ' + MOVE_LIMIT, { size: 80, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 8 }); movesTxt.anchor.set(0.5, 0); LK.gui.top.addChild(movesTxt); movesTxt.y = 90; levelTxt = new Text2('Level: ' + level, { size: 80, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 8 }); levelTxt.anchor.set(0.5, 0); levelTxt.x = BOARD_OFFSET_X + CELL_SIZE * BOARD_COLS / 2; levelTxt.y = BOARD_OFFSET_Y + CELL_SIZE * BOARD_ROWS + 40; game.addChild(levelTxt); objectiveTxt = new Text2('Target: ' + objective.value, { size: 80, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 8 }); objectiveTxt.anchor.set(0.5, 0); objectiveTxt.x = BOARD_OFFSET_X + CELL_SIZE * BOARD_COLS / 2; objectiveTxt.y = BOARD_OFFSET_Y + CELL_SIZE * BOARD_ROWS + 140; game.addChild(objectiveTxt); // --- Board Initialization --- function createBoard() { // Clear previous for (var i = 0; i < candies.length; ++i) { candies[i].destroy(); } if (bomb) { bomb.destroy(); bomb = null; } candies = []; board = []; var levelCandyTypes = getCandyTypesForLevel(level); for (var row = 0; row < BOARD_ROWS; ++row) { board[row] = []; for (var col = 0; col < BOARD_COLS; ++col) { var candy = new Candy(); var type = levelCandyTypes[Math.floor(Math.random() * levelCandyTypes.length)]; candy.setCandy(type); candy.row = row; candy.col = col; candy.x = BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2; candy.y = BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2; candy.isMoving = false; candy.isMatched = false; candy.blocker = false; // No jelly spawn at board creation; jelly will be spawned later like bomb candy.jelly = false; boardContainer.addChild(candy); board[row][col] = candy; candies.push(candy); } } // Remove any initial matches removeInitialMatches(); } function removeInitialMatches() { var found = false; do { found = false; for (var row = 0; row < BOARD_ROWS; ++row) { for (var col = 0; col < BOARD_COLS; ++col) { var candy = board[row][col]; var type = candy.candyType; // Horizontal if (col >= 2 && board[row][col - 1].candyType === type && board[row][col - 2].candyType === type) { var newType = getRandomCandyTypeExcept(type); candy.setCandy(newType); found = true; } // Vertical if (row >= 2 && board[row - 1][col].candyType === type && board[row - 2][col].candyType === type) { var newType = getRandomCandyTypeExcept(type); candy.setCandy(newType); found = true; } } } } while (found); } function getRandomCandyTypeExcept(type) { var levelCandyTypes = getCandyTypesForLevel(level); var arr = []; for (var i = 0; i < levelCandyTypes.length; ++i) { if (levelCandyTypes[i] !== type) arr.push(levelCandyTypes[i]); } return arr[Math.floor(Math.random() * arr.length)]; } // --- Input Handling --- game.down = function (x, y, obj) { if (!canInput) return; // Check if bomb is touched if (bomb && Math.abs(x - bomb.x) < CELL_SIZE / 2 && Math.abs(y - bomb.y) < CELL_SIZE / 2) { bombDragging = true; return; } var pos = getBoardCellFromPos(x, y); if (!pos || typeof board[pos.row] === "undefined" || typeof board[pos.row][pos.col] === "undefined") return; var candy = board[pos.row][pos.col]; if (candy.blocker) return; selectedCandy = candy; }; game.move = function (x, y, obj) { if (bombDragging && bomb) { // Drag bomb like a candy: highlight swap target, allow swap with adjacent only var pos = getBoardCellFromPos(x, y); if (!pos) return; var target = board[pos.row][pos.col]; // Only allow swap with adjacent normal candy (not blocker, not special, not bomb itself) if (target && !target.blocker && !target.special && target !== bomb && Math.abs(bomb.row - pos.row) + Math.abs(bomb.col - pos.col) === 1) { // Animate swap var bombOldRow = bomb.row, bombOldCol = bomb.col; var targetOldRow = target.row, targetOldCol = target.col; var bombTargetX = BOARD_OFFSET_X + target.col * CELL_SIZE + CELL_SIZE / 2; var bombTargetY = BOARD_OFFSET_Y + target.row * CELL_SIZE + CELL_SIZE / 2; var targetTargetX = BOARD_OFFSET_X + bomb.col * CELL_SIZE + CELL_SIZE / 2; var targetTargetY = BOARD_OFFSET_Y + bomb.row * CELL_SIZE + CELL_SIZE / 2; // Animate bomb tween(bomb, { x: bombTargetX, y: bombTargetY }, { duration: 180, easing: tween.cubicInOut }); // Animate target tween(target, { x: targetTargetX, y: targetTargetY }, { duration: 180, easing: tween.cubicInOut, onFinish: function onFinish() { // Swap in board board[bombOldRow][bombOldCol] = target; board[targetOldRow][targetOldCol] = bomb; bomb.row = targetOldRow; bomb.col = targetOldCol; target.row = bombOldRow; target.col = bombOldCol; bomb.x = BOARD_OFFSET_X + bomb.col * CELL_SIZE + CELL_SIZE / 2; bomb.y = BOARD_OFFSET_Y + bomb.row * CELL_SIZE + CELL_SIZE / 2; target.x = BOARD_OFFSET_X + target.col * CELL_SIZE + CELL_SIZE / 2; target.y = BOARD_OFFSET_Y + target.row * CELL_SIZE + CELL_SIZE / 2; // After swap, check for bomb activation (if bomb is now next to a match group) var neighbors = [{ r: bomb.row - 1, c: bomb.col }, { r: bomb.row + 1, c: bomb.col }, { r: bomb.row, c: bomb.col - 1 }, { r: bomb.row, c: bomb.col + 1 }]; var foundMatch = false; for (var i = 0; i < neighbors.length; ++i) { var n = neighbors[i]; if (n.r >= 0 && n.r < BOARD_ROWS && n.c >= 0 && n.c < BOARD_COLS) { var neighbor = board[n.r][n.c]; if (neighbor && !neighbor.blocker && !neighbor.special && neighbor.candyType === target.candyType) { foundMatch = true; break; } } } if (foundMatch) { // Activate bomb: pop all candies in 3x3 area centered on bomb var popped = []; for (var dr = -1; dr <= 1; ++dr) { for (var dc = -1; dc <= 1; ++dc) { var rr = bomb.row + dr, cc = bomb.col + dc; if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) { var c = board[rr][cc]; if (c && !c.blocker) { c.isMatched = true; popped.push(c); } } } } // Animate bomb pop effect var effect = LK.getAsset('comboEffect5', { anchorX: 0.5, anchorY: 0.5, x: bomb.x, y: bomb.y, alpha: 0.95, scaleX: 1.2, scaleY: 1.2 }); game.addChild(effect); tween(effect, { scaleX: 2.2, scaleY: 2.2, alpha: 0 }, { duration: 400, easing: tween.cubicOut, onFinish: function onFinish() { effect.destroy(); } }); // Remove bomb bomb.destroy(); bomb = null; bombDragging = false; // Remove candies and cascade handleMatches(popped); } else { // If no match, just finish drag bombDragging = false; } } }); bombDragging = false; } return; } if (!canInput || !selectedCandy) return; var pos = getBoardCellFromPos(x, y); if (!pos) return; var targetCandy = board[pos.row][pos.col]; if (targetCandy === selectedCandy) return; if (targetCandy.blocker) return; // Only allow swap with adjacent if (isAdjacent(selectedCandy, targetCandy)) { swappingCandy = targetCandy; swapCandies(selectedCandy, swappingCandy, true); selectedCandy = null; swappingCandy = null; } }; game.up = function (x, y, obj) { selectedCandy = null; swappingCandy = null; bombDragging = false; }; // --- Board Utility --- function getBoardCellFromPos(x, y) { var col = Math.floor((x - BOARD_OFFSET_X) / CELL_SIZE); var row = Math.floor((y - BOARD_OFFSET_Y) / CELL_SIZE); if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return null; return { row: row, col: col }; } function isAdjacent(a, b) { return Math.abs(a.row - b.row) + Math.abs(a.col - b.col) === 1; } // --- Swap Logic --- function swapCandies(a, b, playerMove) { canInput = false; // Play swap sound LK.getSound('swap').play(); // Animate swap var ax = a.x, ay = a.y, bx = b.x, by = b.y; tween(a, { x: bx, y: by }, { duration: 180, easing: tween.cubicInOut }); tween(b, { x: ax, y: ay }, { duration: 180, easing: tween.cubicInOut, onFinish: function onFinish() { // Swap in board var tempRow = a.row, tempCol = a.col; board[a.row][a.col] = b; board[b.row][b.col] = a; a.row = b.row; a.col = b.col; b.row = tempRow; b.col = tempCol; // Check for matches var matched = findAndMarkMatches(); if (matched.length > 0) { if (playerMove) { movesLeft--; updateMoves(); } handleMatches(matched); } else { // No match: swap back if player move if (playerMove) { tween(a, { x: ax, y: ay }, { duration: 180, easing: tween.cubicInOut }); tween(b, { x: bx, y: by }, { duration: 180, easing: tween.cubicInOut, onFinish: function onFinish() { // Swap back in board var tempRow2 = a.row, tempCol2 = a.col; board[a.row][a.col] = b; board[b.row][b.col] = a; a.row = b.row; a.col = b.col; b.row = tempRow2; b.col = tempCol2; canInput = true; } }); } else { canInput = true; } } } }); } // --- Match Detection --- function findAndMarkMatches() { var matched = []; // Reset for (var i = 0; i < candies.length; ++i) candies[i].isMatched = false; // Horizontal for (var row = 0; row < BOARD_ROWS; ++row) { var count = 1; for (var col = 1; col < BOARD_COLS; ++col) { var prev = board[row][col - 1]; var curr = board[row][col]; if (curr.candyType === prev.candyType && !curr.blocker && !prev.blocker) { count++; } else { if (count >= 3) { for (var k = 0; k < count; ++k) { board[row][col - 1 - k].isMatched = true; matched.push(board[row][col - 1 - k]); } } count = 1; } } if (count >= 3) { for (var k = 0; k < count; ++k) { board[row][BOARD_COLS - 1 - k].isMatched = true; matched.push(board[row][BOARD_COLS - 1 - k]); } } } // Vertical for (var col = 0; col < BOARD_COLS; ++col) { var count = 1; for (var row = 1; row < BOARD_ROWS; ++row) { var prev = board[row - 1][col]; var curr = board[row][col]; if (curr.candyType === prev.candyType && !curr.blocker && !prev.blocker) { count++; } else { if (count >= 3) { for (var k = 0; k < count; ++k) { board[row - 1 - k][col].isMatched = true; matched.push(board[row - 1 - k][col]); } } count = 1; } } if (count >= 3) { for (var k = 0; k < count; ++k) { board[BOARD_ROWS - 1 - k][col].isMatched = true; matched.push(board[BOARD_ROWS - 1 - k][col]); } } } return matched; } // --- Handle Matches, Remove, Cascade --- function handleMatches(matched) { if (matched.length === 0) { // Reset combo if no match comboCount = 0; comboTxt.setText(''); if (comboTimer) { LK.clearTimeout(comboTimer); comboTimer = null; } canInput = true; return; } // --- Combo System --- comboCount++; comboTxt.setText('Combo X' + comboCount); // Animate combo text for feedback comboTxt.alpha = 1; comboTxt.scaleX = comboTxt.scaleY = 1 + 0.15 * Math.min(comboCount, 5); tween(comboTxt, { scaleX: 1, scaleY: 1, alpha: 1 }, { duration: 200, easing: tween.cubicOut }); // Reset combo after 1.2s if no new match if (comboTimer) LK.clearTimeout(comboTimer); comboTimer = LK.setTimeout(function () { comboCount = 0; comboTxt.setText(''); comboTimer = null; }, 1200); // Play match sound LK.getSound('match').play(); // --- Candy pop effect --- for (var i = 0; i < matched.length; ++i) { var c = matched[i]; // Pop effect: scale up, then shrink and fade tween(c, { scaleX: 1.1 + 0.1 * Math.min(comboCount, 5), scaleY: 1.1 + 0.1 * Math.min(comboCount, 5) }, { duration: 80, easing: tween.cubicOut, onFinish: function (c) { return function () { tween(c, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 180, easing: tween.cubicIn, onFinish: function onFinish() { // Remove from board } }); // --- Combo effect: show burst/flash on board as image asset --- // Choose effect asset based on comboCount var effectAssetId = 'comboEffect1'; if (comboCount >= 5) { effectAssetId = 'comboEffect5'; } else if (comboCount >= 4) { effectAssetId = 'comboEffect4'; } else if (comboCount >= 3) { effectAssetId = 'comboEffect3'; } else if (comboCount >= 2) { effectAssetId = 'comboEffect2'; } var effect = LK.getAsset(effectAssetId, { anchorX: 0.5, anchorY: 0.5, x: c.x, y: c.y, alpha: 0.85, scaleX: 1, scaleY: 1 }); game.addChild(effect); tween(effect, { scaleX: 1.2 + 0.2 * Math.min(comboCount, 5), scaleY: 1.2 + 0.2 * Math.min(comboCount, 5), alpha: 0 }, { duration: 350 + 80 * Math.min(comboCount, 5), easing: tween.cubicOut, onFinish: function onFinish() { effect.destroy(); } }); }; }(c) }); } // Add score (bonus for combo) score += matched.length * 60 + (comboCount > 1 ? matched.length * 20 * (comboCount - 1) : 0); updateScore(); // Remove after animation LK.setTimeout(function () { for (var i = 0; i < matched.length; ++i) { var c = matched[i]; board[c.row][c.col] = null; c.destroy(); var idx = candies.indexOf(c); if (idx !== -1) candies.splice(idx, 1); } cascadeCandies(); }, 200); } // --- Cascade (Gravity) --- function cascadeCandies() { // For each column, move candies down to fill gaps var moved = false; // Play cascade sound when cascade starts LK.getSound('cascade').play(); for (var col = 0; col < BOARD_COLS; ++col) { for (var row = BOARD_ROWS - 1; row >= 0; --row) { if (!board[row][col]) { // Find first non-null above for (var r = row - 1; r >= 0; --r) { if (board[r][col]) { var c = board[r][col]; board[row][col] = c; board[r][col] = null; var oldY = c.y; c.row = row; tween(c, { y: BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2 }, { duration: 180, easing: tween.cubicIn }); moved = true; break; } } } } } // Fill empty spots at top var levelCandyTypes = getCandyTypesForLevel(level); for (var col = 0; col < BOARD_COLS; ++col) { for (var row = 0; row < BOARD_ROWS; ++row) { if (!board[row][col]) { // --- Bomb spawn logic: 2% chance, only if no bomb exists --- var spawnBombHere = false; if (!bomb && Math.random() < 0.02) { spawnBombHere = true; } if (spawnBombHere) { bomb = new CandyBomb(); bomb.setPosition(row, col); bomb.y = BOARD_OFFSET_Y - CELL_SIZE + row * CELL_SIZE + CELL_SIZE / 2; boardContainer.addChild(bomb); board[row][col] = bomb; tween(bomb, { y: BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2 }, { duration: 180, easing: tween.cubicIn }); } else { var candy = new Candy(); var type = levelCandyTypes[Math.floor(Math.random() * levelCandyTypes.length)]; candy.setCandy(type); candy.row = row; candy.col = col; candy.x = BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2; candy.y = BOARD_OFFSET_Y - CELL_SIZE + row * CELL_SIZE + CELL_SIZE / 2; // --- Jelly spawn logic: 3% chance per cell, spawn as standalone jelly (not inside a candy) --- if (Math.random() < 0.03) { // Spawn a standalone jelly (no candy in this cell) var jellyObj = new Container(); jellyObj.row = row; jellyObj.col = col; jellyObj.isJelly = true; jellyObj.asset = jellyObj.attachAsset('jelly', { anchorX: 0.5, anchorY: 0.5, alpha: 0.85 }); jellyObj.x = BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2; jellyObj.y = BOARD_OFFSET_Y - CELL_SIZE + row * CELL_SIZE + CELL_SIZE / 2; boardContainer.addChild(jellyObj); board[row][col] = jellyObj; tween(jellyObj, { y: BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2 }, { duration: 180, easing: tween.cubicIn }); } else { var candy = new Candy(); var type = levelCandyTypes[Math.floor(Math.random() * levelCandyTypes.length)]; candy.setCandy(type); candy.row = row; candy.col = col; candy.x = BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2; candy.y = BOARD_OFFSET_Y - CELL_SIZE + row * CELL_SIZE + CELL_SIZE / 2; boardContainer.addChild(candy); candies.push(candy); board[row][col] = candy; tween(candy, { y: BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2 }, { duration: 180, easing: tween.cubicIn }); } } } } } // After all movement, check for new matches LK.setTimeout(function () { var matched = findAndMarkMatches(); if (matched.length > 0) { handleMatches(matched); } else { // --- Bomb spawn logic: 2% chance after cascade, only if no bomb exists --- if (!bomb && Math.random() < 0.02) { // Find a random empty cell to place the bomb var emptyCells = []; for (var row = 0; row < BOARD_ROWS; ++row) { for (var col = 0; col < BOARD_COLS; ++col) { if (board[row][col] && !board[row][col].blocker && !board[row][col].special && !board[row][col].isMatched) { emptyCells.push({ row: row, col: col }); } } } if (emptyCells.length > 0) { var idx = Math.floor(Math.random() * emptyCells.length); var pos = emptyCells[idx]; bomb = new CandyBomb(); bomb.setPosition(pos.row, pos.col); boardContainer.addChild(bomb); } } checkEndConditions(); canInput = true; } }, 200); } // --- Buff System --- var roundBuff = null; var buffCards = []; var buffOverlay = null; var BUFFS = [{ id: "extraMove", desc: "+3 Moves this round" }, { id: "scoreBoost", desc: "+30% Score this round" }, { id: "comboStart", desc: "Start with Combo X2" }, { id: "easyObjective", desc: "Objective -20% this round" }, { id: "bombChance", desc: "Bomb spawn chance +5% this round" }, { id: "jellyClear", desc: "Clear 3 random jellies" }]; function showBuffSelection(onBuffSelected) { if (buffOverlay) buffOverlay.destroy(); buffOverlay = new Container(); buffOverlay.x = 0; buffOverlay.y = 0; game.addChild(buffOverlay); // Dim background var dim = LK.getAsset('boardBg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 2732, alpha: 0.7 }); buffOverlay.addChild(dim); // Pick 3 unique random buffs var available = BUFFS.slice(); var chosen = []; for (var i = 0; i < 3; ++i) { var idx = Math.floor(Math.random() * available.length); chosen.push(available[idx]); available.splice(idx, 1); } buffCards = []; var spacing = 500; var startX = 2048 / 2 - spacing; for (var i = 0; i < 3; ++i) { var card = new BuffCard(); card.setBuff(chosen[i].id, chosen[i].desc); card.x = startX + i * spacing; card.y = 1200; card.onSelect = function (buffId) { // Remove overlay/cards for (var j = 0; j < buffCards.length; ++j) { buffCards[j].destroy(); } buffCards = []; if (buffOverlay) { buffOverlay.destroy(); buffOverlay = null; } roundBuff = buffId; if (typeof onBuffSelected === "function") onBuffSelected(buffId); }; buffOverlay.addChild(card); buffCards.push(card); } // Title var title = new Text2("Choose a Buff for this round!", { size: 100, fill: 0xffffff, stroke: 0x000000, strokeThickness: 10 }); title.anchor.set(0.5, 0.5); title.x = 2048 / 2; title.y = 950; buffOverlay.addChild(title); } // --- Score, Moves, Level --- function updateScore() { var val = score; if (roundBuff === "scoreBoost") val = Math.floor(val * 1.3); scoreTxt.setText('Score: ' + val); } function updateMoves() { var val = movesLeft; if (roundBuff === "extraMove") val += 3; movesTxt.setText('Moves: ' + val); } // --- End Conditions --- function checkEndConditions() { if (objective.type === 'score' && score >= objective.value) { // Win storage.level = level + 1; roundBuff = null; LK.showYouWin(); } else if (movesLeft <= 0) { roundBuff = null; LK.showGameOver(); } } // --- Game Update --- game.update = function () { // No per-frame logic needed for MVP }; // --- Start Game --- function startGame() { // On level up, show buff selection if (typeof window._lastLevel === "undefined" || window._lastLevel !== level) { window._lastLevel = level; showBuffSelection(function (buffId) { roundBuff = buffId; _startGameWithBuff(); }); } else { _startGameWithBuff(); } } function _startGameWithBuff() { score = 0; movesLeft = MOVE_LIMIT; objective.value = 2000 + (level - 1) * 400; // Apply buff effects if (roundBuff === "easyObjective") { objective.value = Math.floor(objective.value * 0.8); } if (roundBuff === "extraMove") { movesLeft += 3; } if (roundBuff === "comboStart") { comboCount = 2; comboTxt.setText('Combo X2'); } if (roundBuff === "jellyClear") { // Remove up to 3 random jellies var jellies = []; for (var i = 0; i < candies.length; ++i) { if (candies[i].jelly) jellies.push(candies[i]); } for (var j = 0; j < 3 && jellies.length > 0; ++j) { var idx = Math.floor(Math.random() * jellies.length); var c = jellies[idx]; c.hideJelly(); c.jelly = false; jellies.splice(idx, 1); } } updateScore(); updateMoves(); levelTxt.setText('Level: ' + level); if (levelTxt.style) { levelTxt.style.stroke = 0x000000; levelTxt.style.strokeThickness = 8; } objectiveTxt.setText('Target: ' + objective.value); if (objectiveTxt.style) { objectiveTxt.style.stroke = 0x000000; objectiveTxt.style.strokeThickness = 8; } if (scoreTxt.style) { scoreTxt.style.stroke = 0x000000; scoreTxt.style.strokeThickness = 8; } if (movesTxt.style) { movesTxt.style.stroke = 0x000000; movesTxt.style.strokeThickness = 8; } if (comboTxt.style) { comboTxt.style.stroke = 0x000000; comboTxt.style.strokeThickness = 8; } createBoard(); canInput = true; } startGame();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
level: 1
});
/****
* Classes
****/
// --- BuffCard Class ---
var BuffCard = Container.expand(function () {
var self = Container.call(this);
self.buffId = null;
self.buffDesc = '';
self.selected = false;
self.bg = self.attachAsset('boardBg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.1,
scaleY: 1.4,
alpha: 0.93
});
// Add image asset for the card (will be set in setBuff)
self.image = null;
// Text for description
self.text = new Text2('', {
size: 60,
fill: 0xffffff,
stroke: 0x000000,
strokeThickness: 8,
wordWrap: true,
wordWrapWidth: 320
});
self.text.anchor.set(0.5, 0.5);
self.text.y = 120; // Move text down to make space for image
self.addChild(self.text);
// Map buffId to image asset id (add your own asset ids as needed)
var BUFF_IMAGE_MAP = {
extraMove: 'candyBlue',
scoreBoost: 'candyYellow',
comboStart: 'candyRed',
easyObjective: 'candyGreen',
bombChance: 'candyBomb',
jellyClear: 'jelly'
};
self.setBuff = function (buffId, desc) {
self.buffId = buffId;
self.buffDesc = desc;
self.text.setText(desc);
if (self.text.style) {
self.text.style.stroke = 0x000000;
self.text.style.strokeThickness = 8;
}
// Remove previous image if any
if (self.image) {
self.removeChild(self.image);
self.image = null;
}
// Add image asset for this buff
var assetId = BUFF_IMAGE_MAP[buffId];
if (assetId) {
self.image = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
y: -40,
// Place image above text
scaleX: 0.7,
scaleY: 0.7
});
}
};
self.down = function (x, y, obj) {
if (self.selected) return;
self.selected = true;
if (typeof self.onSelect === "function") self.onSelect(self.buffId);
};
return self;
});
// Candy class
var Candy = Container.expand(function () {
var self = Container.call(this);
self.candyType = null; // e.g. 'red', 'green', etc.
self.special = null; // null, 'stripedH', 'stripedV', 'wrapped', 'bomb'
self.row = 0;
self.col = 0;
self.isMoving = false;
self.isMatched = false;
self.blocker = false;
self.jelly = false;
self.asset = null;
self.setCandy = function (type, special) {
self.candyType = type;
self.special = special || null;
if (self.asset) {
self.removeChild(self.asset);
}
var assetId = 'candy' + type.charAt(0).toUpperCase() + type.slice(1);
if (special === 'stripedH') assetId = 'candyStripedH';
if (special === 'stripedV') assetId = 'candyStripedV';
if (special === 'wrapped') assetId = 'candyWrapped';
if (special === 'bomb') assetId = 'candyBomb';
self.asset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
};
self.showJelly = function () {
if (!self.jellyAsset) {
self.jellyAsset = self.attachAsset('jelly', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5
});
}
};
self.hideJelly = function () {
if (self.jellyAsset) {
self.removeChild(self.jellyAsset);
self.jellyAsset = null;
}
};
self.showBlocker = function () {
if (!self.blockerAsset) {
self.blockerAsset = self.attachAsset('blocker', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.7
});
}
};
self.hideBlocker = function () {
if (self.blockerAsset) {
self.removeChild(self.blockerAsset);
self.blockerAsset = null;
}
};
return self;
});
// --- CandyBomb Class ---
var CandyBomb = Container.expand(function () {
var self = Container.call(this);
self.row = 0;
self.col = 0;
self.isMoving = false;
self.asset = self.attachAsset('candyBomb', {
anchorX: 0.5,
anchorY: 0.5
});
self.setPosition = function (row, col) {
self.row = row;
self.col = col;
self.x = BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2;
self.y = BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2;
};
self.updateBoardPos = function () {
self.row = Math.round((self.y - BOARD_OFFSET_Y - CELL_SIZE / 2) / CELL_SIZE);
self.col = Math.round((self.x - BOARD_OFFSET_X - CELL_SIZE / 2) / CELL_SIZE);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
// No title, no description
// Always backgroundColor is black
backgroundColor: 0x000000
});
/****
* Game Code
****/
// --- Board Settings ---
// Candy shapes/colors
// Special candies
// Board background
// Jelly overlay
// Blocker
// Sound assets
// Combo effect image assets (replace with your own asset ids as needed)
var BOARD_ROWS = 9;
var BOARD_COLS = 9;
var CELL_SIZE = 180;
var BOARD_OFFSET_X = (2048 - CELL_SIZE * BOARD_COLS) / 2;
var BOARD_OFFSET_Y = 350;
var CANDY_TYPES = ['Red', 'Green', 'Blue', 'Yellow', 'Purple', 'Orange'];
var MOVE_LIMIT = 25;
// --- Game State ---
var board = [];
var candies = [];
var selectedCandy = null;
var swappingCandy = null;
var canInput = true;
var movesLeft = MOVE_LIMIT;
var score = 0;
var level = storage.level || 1;
var objective = {
type: 'score',
value: 2000 + (level - 1) * 400 // Target increases with level
}; // MVP: score target
// --- Bomb System ---
var bomb = null;
var bombDragging = false;
// Adjust candy types for higher levels (more types as level increases, max 6)
var CANDY_TYPES = ['Red', 'Green', 'Blue', 'Yellow', 'Purple', 'Orange'];
function getCandyTypesForLevel(lvl) {
var count = 4 + Math.floor((lvl - 1) / 2);
if (count > 6) count = 6;
return CANDY_TYPES.slice(0, count);
}
var scoreTxt, movesTxt, levelTxt, objectiveTxt;
// --- Combo System ---
var comboCount = 0;
var comboTimer = null;
var comboTxt = new Text2('', {
size: 90,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 8
});
comboTxt.anchor.set(0, 0);
comboTxt.x = BOARD_OFFSET_X - 180;
comboTxt.y = BOARD_OFFSET_Y + 40;
game.addChild(comboTxt);
// --- Board Container ---
// Add background image to game, behind everything
var mainBg = LK.getAsset('mainBg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChild(mainBg);
var boardContainer = new Container();
game.addChild(boardContainer);
// --- Board Background ---
var boardBg = LK.getAsset('boardBg', {
anchorX: 0,
anchorY: 0,
x: BOARD_OFFSET_X,
y: BOARD_OFFSET_Y,
width: CELL_SIZE * BOARD_COLS,
height: CELL_SIZE * BOARD_ROWS
});
boardContainer.addChildAt(boardBg, 0);
// --- GUI ---
scoreTxt = new Text2('Score: 0', {
size: 80,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 8
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
movesTxt = new Text2('Moves: ' + MOVE_LIMIT, {
size: 80,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 8
});
movesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(movesTxt);
movesTxt.y = 90;
levelTxt = new Text2('Level: ' + level, {
size: 80,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 8
});
levelTxt.anchor.set(0.5, 0);
levelTxt.x = BOARD_OFFSET_X + CELL_SIZE * BOARD_COLS / 2;
levelTxt.y = BOARD_OFFSET_Y + CELL_SIZE * BOARD_ROWS + 40;
game.addChild(levelTxt);
objectiveTxt = new Text2('Target: ' + objective.value, {
size: 80,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 8
});
objectiveTxt.anchor.set(0.5, 0);
objectiveTxt.x = BOARD_OFFSET_X + CELL_SIZE * BOARD_COLS / 2;
objectiveTxt.y = BOARD_OFFSET_Y + CELL_SIZE * BOARD_ROWS + 140;
game.addChild(objectiveTxt);
// --- Board Initialization ---
function createBoard() {
// Clear previous
for (var i = 0; i < candies.length; ++i) {
candies[i].destroy();
}
if (bomb) {
bomb.destroy();
bomb = null;
}
candies = [];
board = [];
var levelCandyTypes = getCandyTypesForLevel(level);
for (var row = 0; row < BOARD_ROWS; ++row) {
board[row] = [];
for (var col = 0; col < BOARD_COLS; ++col) {
var candy = new Candy();
var type = levelCandyTypes[Math.floor(Math.random() * levelCandyTypes.length)];
candy.setCandy(type);
candy.row = row;
candy.col = col;
candy.x = BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2;
candy.y = BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2;
candy.isMoving = false;
candy.isMatched = false;
candy.blocker = false;
// No jelly spawn at board creation; jelly will be spawned later like bomb
candy.jelly = false;
boardContainer.addChild(candy);
board[row][col] = candy;
candies.push(candy);
}
}
// Remove any initial matches
removeInitialMatches();
}
function removeInitialMatches() {
var found = false;
do {
found = false;
for (var row = 0; row < BOARD_ROWS; ++row) {
for (var col = 0; col < BOARD_COLS; ++col) {
var candy = board[row][col];
var type = candy.candyType;
// Horizontal
if (col >= 2 && board[row][col - 1].candyType === type && board[row][col - 2].candyType === type) {
var newType = getRandomCandyTypeExcept(type);
candy.setCandy(newType);
found = true;
}
// Vertical
if (row >= 2 && board[row - 1][col].candyType === type && board[row - 2][col].candyType === type) {
var newType = getRandomCandyTypeExcept(type);
candy.setCandy(newType);
found = true;
}
}
}
} while (found);
}
function getRandomCandyTypeExcept(type) {
var levelCandyTypes = getCandyTypesForLevel(level);
var arr = [];
for (var i = 0; i < levelCandyTypes.length; ++i) {
if (levelCandyTypes[i] !== type) arr.push(levelCandyTypes[i]);
}
return arr[Math.floor(Math.random() * arr.length)];
}
// --- Input Handling ---
game.down = function (x, y, obj) {
if (!canInput) return;
// Check if bomb is touched
if (bomb && Math.abs(x - bomb.x) < CELL_SIZE / 2 && Math.abs(y - bomb.y) < CELL_SIZE / 2) {
bombDragging = true;
return;
}
var pos = getBoardCellFromPos(x, y);
if (!pos || typeof board[pos.row] === "undefined" || typeof board[pos.row][pos.col] === "undefined") return;
var candy = board[pos.row][pos.col];
if (candy.blocker) return;
selectedCandy = candy;
};
game.move = function (x, y, obj) {
if (bombDragging && bomb) {
// Drag bomb like a candy: highlight swap target, allow swap with adjacent only
var pos = getBoardCellFromPos(x, y);
if (!pos) return;
var target = board[pos.row][pos.col];
// Only allow swap with adjacent normal candy (not blocker, not special, not bomb itself)
if (target && !target.blocker && !target.special && target !== bomb && Math.abs(bomb.row - pos.row) + Math.abs(bomb.col - pos.col) === 1) {
// Animate swap
var bombOldRow = bomb.row,
bombOldCol = bomb.col;
var targetOldRow = target.row,
targetOldCol = target.col;
var bombTargetX = BOARD_OFFSET_X + target.col * CELL_SIZE + CELL_SIZE / 2;
var bombTargetY = BOARD_OFFSET_Y + target.row * CELL_SIZE + CELL_SIZE / 2;
var targetTargetX = BOARD_OFFSET_X + bomb.col * CELL_SIZE + CELL_SIZE / 2;
var targetTargetY = BOARD_OFFSET_Y + bomb.row * CELL_SIZE + CELL_SIZE / 2;
// Animate bomb
tween(bomb, {
x: bombTargetX,
y: bombTargetY
}, {
duration: 180,
easing: tween.cubicInOut
});
// Animate target
tween(target, {
x: targetTargetX,
y: targetTargetY
}, {
duration: 180,
easing: tween.cubicInOut,
onFinish: function onFinish() {
// Swap in board
board[bombOldRow][bombOldCol] = target;
board[targetOldRow][targetOldCol] = bomb;
bomb.row = targetOldRow;
bomb.col = targetOldCol;
target.row = bombOldRow;
target.col = bombOldCol;
bomb.x = BOARD_OFFSET_X + bomb.col * CELL_SIZE + CELL_SIZE / 2;
bomb.y = BOARD_OFFSET_Y + bomb.row * CELL_SIZE + CELL_SIZE / 2;
target.x = BOARD_OFFSET_X + target.col * CELL_SIZE + CELL_SIZE / 2;
target.y = BOARD_OFFSET_Y + target.row * CELL_SIZE + CELL_SIZE / 2;
// After swap, check for bomb activation (if bomb is now next to a match group)
var neighbors = [{
r: bomb.row - 1,
c: bomb.col
}, {
r: bomb.row + 1,
c: bomb.col
}, {
r: bomb.row,
c: bomb.col - 1
}, {
r: bomb.row,
c: bomb.col + 1
}];
var foundMatch = false;
for (var i = 0; i < neighbors.length; ++i) {
var n = neighbors[i];
if (n.r >= 0 && n.r < BOARD_ROWS && n.c >= 0 && n.c < BOARD_COLS) {
var neighbor = board[n.r][n.c];
if (neighbor && !neighbor.blocker && !neighbor.special && neighbor.candyType === target.candyType) {
foundMatch = true;
break;
}
}
}
if (foundMatch) {
// Activate bomb: pop all candies in 3x3 area centered on bomb
var popped = [];
for (var dr = -1; dr <= 1; ++dr) {
for (var dc = -1; dc <= 1; ++dc) {
var rr = bomb.row + dr,
cc = bomb.col + dc;
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
var c = board[rr][cc];
if (c && !c.blocker) {
c.isMatched = true;
popped.push(c);
}
}
}
}
// Animate bomb pop effect
var effect = LK.getAsset('comboEffect5', {
anchorX: 0.5,
anchorY: 0.5,
x: bomb.x,
y: bomb.y,
alpha: 0.95,
scaleX: 1.2,
scaleY: 1.2
});
game.addChild(effect);
tween(effect, {
scaleX: 2.2,
scaleY: 2.2,
alpha: 0
}, {
duration: 400,
easing: tween.cubicOut,
onFinish: function onFinish() {
effect.destroy();
}
});
// Remove bomb
bomb.destroy();
bomb = null;
bombDragging = false;
// Remove candies and cascade
handleMatches(popped);
} else {
// If no match, just finish drag
bombDragging = false;
}
}
});
bombDragging = false;
}
return;
}
if (!canInput || !selectedCandy) return;
var pos = getBoardCellFromPos(x, y);
if (!pos) return;
var targetCandy = board[pos.row][pos.col];
if (targetCandy === selectedCandy) return;
if (targetCandy.blocker) return;
// Only allow swap with adjacent
if (isAdjacent(selectedCandy, targetCandy)) {
swappingCandy = targetCandy;
swapCandies(selectedCandy, swappingCandy, true);
selectedCandy = null;
swappingCandy = null;
}
};
game.up = function (x, y, obj) {
selectedCandy = null;
swappingCandy = null;
bombDragging = false;
};
// --- Board Utility ---
function getBoardCellFromPos(x, y) {
var col = Math.floor((x - BOARD_OFFSET_X) / CELL_SIZE);
var row = Math.floor((y - BOARD_OFFSET_Y) / CELL_SIZE);
if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return null;
return {
row: row,
col: col
};
}
function isAdjacent(a, b) {
return Math.abs(a.row - b.row) + Math.abs(a.col - b.col) === 1;
}
// --- Swap Logic ---
function swapCandies(a, b, playerMove) {
canInput = false;
// Play swap sound
LK.getSound('swap').play();
// Animate swap
var ax = a.x,
ay = a.y,
bx = b.x,
by = b.y;
tween(a, {
x: bx,
y: by
}, {
duration: 180,
easing: tween.cubicInOut
});
tween(b, {
x: ax,
y: ay
}, {
duration: 180,
easing: tween.cubicInOut,
onFinish: function onFinish() {
// Swap in board
var tempRow = a.row,
tempCol = a.col;
board[a.row][a.col] = b;
board[b.row][b.col] = a;
a.row = b.row;
a.col = b.col;
b.row = tempRow;
b.col = tempCol;
// Check for matches
var matched = findAndMarkMatches();
if (matched.length > 0) {
if (playerMove) {
movesLeft--;
updateMoves();
}
handleMatches(matched);
} else {
// No match: swap back if player move
if (playerMove) {
tween(a, {
x: ax,
y: ay
}, {
duration: 180,
easing: tween.cubicInOut
});
tween(b, {
x: bx,
y: by
}, {
duration: 180,
easing: tween.cubicInOut,
onFinish: function onFinish() {
// Swap back in board
var tempRow2 = a.row,
tempCol2 = a.col;
board[a.row][a.col] = b;
board[b.row][b.col] = a;
a.row = b.row;
a.col = b.col;
b.row = tempRow2;
b.col = tempCol2;
canInput = true;
}
});
} else {
canInput = true;
}
}
}
});
}
// --- Match Detection ---
function findAndMarkMatches() {
var matched = [];
// Reset
for (var i = 0; i < candies.length; ++i) candies[i].isMatched = false;
// Horizontal
for (var row = 0; row < BOARD_ROWS; ++row) {
var count = 1;
for (var col = 1; col < BOARD_COLS; ++col) {
var prev = board[row][col - 1];
var curr = board[row][col];
if (curr.candyType === prev.candyType && !curr.blocker && !prev.blocker) {
count++;
} else {
if (count >= 3) {
for (var k = 0; k < count; ++k) {
board[row][col - 1 - k].isMatched = true;
matched.push(board[row][col - 1 - k]);
}
}
count = 1;
}
}
if (count >= 3) {
for (var k = 0; k < count; ++k) {
board[row][BOARD_COLS - 1 - k].isMatched = true;
matched.push(board[row][BOARD_COLS - 1 - k]);
}
}
}
// Vertical
for (var col = 0; col < BOARD_COLS; ++col) {
var count = 1;
for (var row = 1; row < BOARD_ROWS; ++row) {
var prev = board[row - 1][col];
var curr = board[row][col];
if (curr.candyType === prev.candyType && !curr.blocker && !prev.blocker) {
count++;
} else {
if (count >= 3) {
for (var k = 0; k < count; ++k) {
board[row - 1 - k][col].isMatched = true;
matched.push(board[row - 1 - k][col]);
}
}
count = 1;
}
}
if (count >= 3) {
for (var k = 0; k < count; ++k) {
board[BOARD_ROWS - 1 - k][col].isMatched = true;
matched.push(board[BOARD_ROWS - 1 - k][col]);
}
}
}
return matched;
}
// --- Handle Matches, Remove, Cascade ---
function handleMatches(matched) {
if (matched.length === 0) {
// Reset combo if no match
comboCount = 0;
comboTxt.setText('');
if (comboTimer) {
LK.clearTimeout(comboTimer);
comboTimer = null;
}
canInput = true;
return;
}
// --- Combo System ---
comboCount++;
comboTxt.setText('Combo X' + comboCount);
// Animate combo text for feedback
comboTxt.alpha = 1;
comboTxt.scaleX = comboTxt.scaleY = 1 + 0.15 * Math.min(comboCount, 5);
tween(comboTxt, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 200,
easing: tween.cubicOut
});
// Reset combo after 1.2s if no new match
if (comboTimer) LK.clearTimeout(comboTimer);
comboTimer = LK.setTimeout(function () {
comboCount = 0;
comboTxt.setText('');
comboTimer = null;
}, 1200);
// Play match sound
LK.getSound('match').play();
// --- Candy pop effect ---
for (var i = 0; i < matched.length; ++i) {
var c = matched[i];
// Pop effect: scale up, then shrink and fade
tween(c, {
scaleX: 1.1 + 0.1 * Math.min(comboCount, 5),
scaleY: 1.1 + 0.1 * Math.min(comboCount, 5)
}, {
duration: 80,
easing: tween.cubicOut,
onFinish: function (c) {
return function () {
tween(c, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 180,
easing: tween.cubicIn,
onFinish: function onFinish() {
// Remove from board
}
});
// --- Combo effect: show burst/flash on board as image asset ---
// Choose effect asset based on comboCount
var effectAssetId = 'comboEffect1';
if (comboCount >= 5) {
effectAssetId = 'comboEffect5';
} else if (comboCount >= 4) {
effectAssetId = 'comboEffect4';
} else if (comboCount >= 3) {
effectAssetId = 'comboEffect3';
} else if (comboCount >= 2) {
effectAssetId = 'comboEffect2';
}
var effect = LK.getAsset(effectAssetId, {
anchorX: 0.5,
anchorY: 0.5,
x: c.x,
y: c.y,
alpha: 0.85,
scaleX: 1,
scaleY: 1
});
game.addChild(effect);
tween(effect, {
scaleX: 1.2 + 0.2 * Math.min(comboCount, 5),
scaleY: 1.2 + 0.2 * Math.min(comboCount, 5),
alpha: 0
}, {
duration: 350 + 80 * Math.min(comboCount, 5),
easing: tween.cubicOut,
onFinish: function onFinish() {
effect.destroy();
}
});
};
}(c)
});
}
// Add score (bonus for combo)
score += matched.length * 60 + (comboCount > 1 ? matched.length * 20 * (comboCount - 1) : 0);
updateScore();
// Remove after animation
LK.setTimeout(function () {
for (var i = 0; i < matched.length; ++i) {
var c = matched[i];
board[c.row][c.col] = null;
c.destroy();
var idx = candies.indexOf(c);
if (idx !== -1) candies.splice(idx, 1);
}
cascadeCandies();
}, 200);
}
// --- Cascade (Gravity) ---
function cascadeCandies() {
// For each column, move candies down to fill gaps
var moved = false;
// Play cascade sound when cascade starts
LK.getSound('cascade').play();
for (var col = 0; col < BOARD_COLS; ++col) {
for (var row = BOARD_ROWS - 1; row >= 0; --row) {
if (!board[row][col]) {
// Find first non-null above
for (var r = row - 1; r >= 0; --r) {
if (board[r][col]) {
var c = board[r][col];
board[row][col] = c;
board[r][col] = null;
var oldY = c.y;
c.row = row;
tween(c, {
y: BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2
}, {
duration: 180,
easing: tween.cubicIn
});
moved = true;
break;
}
}
}
}
}
// Fill empty spots at top
var levelCandyTypes = getCandyTypesForLevel(level);
for (var col = 0; col < BOARD_COLS; ++col) {
for (var row = 0; row < BOARD_ROWS; ++row) {
if (!board[row][col]) {
// --- Bomb spawn logic: 2% chance, only if no bomb exists ---
var spawnBombHere = false;
if (!bomb && Math.random() < 0.02) {
spawnBombHere = true;
}
if (spawnBombHere) {
bomb = new CandyBomb();
bomb.setPosition(row, col);
bomb.y = BOARD_OFFSET_Y - CELL_SIZE + row * CELL_SIZE + CELL_SIZE / 2;
boardContainer.addChild(bomb);
board[row][col] = bomb;
tween(bomb, {
y: BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2
}, {
duration: 180,
easing: tween.cubicIn
});
} else {
var candy = new Candy();
var type = levelCandyTypes[Math.floor(Math.random() * levelCandyTypes.length)];
candy.setCandy(type);
candy.row = row;
candy.col = col;
candy.x = BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2;
candy.y = BOARD_OFFSET_Y - CELL_SIZE + row * CELL_SIZE + CELL_SIZE / 2;
// --- Jelly spawn logic: 3% chance per cell, spawn as standalone jelly (not inside a candy) ---
if (Math.random() < 0.03) {
// Spawn a standalone jelly (no candy in this cell)
var jellyObj = new Container();
jellyObj.row = row;
jellyObj.col = col;
jellyObj.isJelly = true;
jellyObj.asset = jellyObj.attachAsset('jelly', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.85
});
jellyObj.x = BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2;
jellyObj.y = BOARD_OFFSET_Y - CELL_SIZE + row * CELL_SIZE + CELL_SIZE / 2;
boardContainer.addChild(jellyObj);
board[row][col] = jellyObj;
tween(jellyObj, {
y: BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2
}, {
duration: 180,
easing: tween.cubicIn
});
} else {
var candy = new Candy();
var type = levelCandyTypes[Math.floor(Math.random() * levelCandyTypes.length)];
candy.setCandy(type);
candy.row = row;
candy.col = col;
candy.x = BOARD_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2;
candy.y = BOARD_OFFSET_Y - CELL_SIZE + row * CELL_SIZE + CELL_SIZE / 2;
boardContainer.addChild(candy);
candies.push(candy);
board[row][col] = candy;
tween(candy, {
y: BOARD_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2
}, {
duration: 180,
easing: tween.cubicIn
});
}
}
}
}
}
// After all movement, check for new matches
LK.setTimeout(function () {
var matched = findAndMarkMatches();
if (matched.length > 0) {
handleMatches(matched);
} else {
// --- Bomb spawn logic: 2% chance after cascade, only if no bomb exists ---
if (!bomb && Math.random() < 0.02) {
// Find a random empty cell to place the bomb
var emptyCells = [];
for (var row = 0; row < BOARD_ROWS; ++row) {
for (var col = 0; col < BOARD_COLS; ++col) {
if (board[row][col] && !board[row][col].blocker && !board[row][col].special && !board[row][col].isMatched) {
emptyCells.push({
row: row,
col: col
});
}
}
}
if (emptyCells.length > 0) {
var idx = Math.floor(Math.random() * emptyCells.length);
var pos = emptyCells[idx];
bomb = new CandyBomb();
bomb.setPosition(pos.row, pos.col);
boardContainer.addChild(bomb);
}
}
checkEndConditions();
canInput = true;
}
}, 200);
}
// --- Buff System ---
var roundBuff = null;
var buffCards = [];
var buffOverlay = null;
var BUFFS = [{
id: "extraMove",
desc: "+3 Moves this round"
}, {
id: "scoreBoost",
desc: "+30% Score this round"
}, {
id: "comboStart",
desc: "Start with Combo X2"
}, {
id: "easyObjective",
desc: "Objective -20% this round"
}, {
id: "bombChance",
desc: "Bomb spawn chance +5% this round"
}, {
id: "jellyClear",
desc: "Clear 3 random jellies"
}];
function showBuffSelection(onBuffSelected) {
if (buffOverlay) buffOverlay.destroy();
buffOverlay = new Container();
buffOverlay.x = 0;
buffOverlay.y = 0;
game.addChild(buffOverlay);
// Dim background
var dim = LK.getAsset('boardBg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732,
alpha: 0.7
});
buffOverlay.addChild(dim);
// Pick 3 unique random buffs
var available = BUFFS.slice();
var chosen = [];
for (var i = 0; i < 3; ++i) {
var idx = Math.floor(Math.random() * available.length);
chosen.push(available[idx]);
available.splice(idx, 1);
}
buffCards = [];
var spacing = 500;
var startX = 2048 / 2 - spacing;
for (var i = 0; i < 3; ++i) {
var card = new BuffCard();
card.setBuff(chosen[i].id, chosen[i].desc);
card.x = startX + i * spacing;
card.y = 1200;
card.onSelect = function (buffId) {
// Remove overlay/cards
for (var j = 0; j < buffCards.length; ++j) {
buffCards[j].destroy();
}
buffCards = [];
if (buffOverlay) {
buffOverlay.destroy();
buffOverlay = null;
}
roundBuff = buffId;
if (typeof onBuffSelected === "function") onBuffSelected(buffId);
};
buffOverlay.addChild(card);
buffCards.push(card);
}
// Title
var title = new Text2("Choose a Buff for this round!", {
size: 100,
fill: 0xffffff,
stroke: 0x000000,
strokeThickness: 10
});
title.anchor.set(0.5, 0.5);
title.x = 2048 / 2;
title.y = 950;
buffOverlay.addChild(title);
}
// --- Score, Moves, Level ---
function updateScore() {
var val = score;
if (roundBuff === "scoreBoost") val = Math.floor(val * 1.3);
scoreTxt.setText('Score: ' + val);
}
function updateMoves() {
var val = movesLeft;
if (roundBuff === "extraMove") val += 3;
movesTxt.setText('Moves: ' + val);
}
// --- End Conditions ---
function checkEndConditions() {
if (objective.type === 'score' && score >= objective.value) {
// Win
storage.level = level + 1;
roundBuff = null;
LK.showYouWin();
} else if (movesLeft <= 0) {
roundBuff = null;
LK.showGameOver();
}
}
// --- Game Update ---
game.update = function () {
// No per-frame logic needed for MVP
};
// --- Start Game ---
function startGame() {
// On level up, show buff selection
if (typeof window._lastLevel === "undefined" || window._lastLevel !== level) {
window._lastLevel = level;
showBuffSelection(function (buffId) {
roundBuff = buffId;
_startGameWithBuff();
});
} else {
_startGameWithBuff();
}
}
function _startGameWithBuff() {
score = 0;
movesLeft = MOVE_LIMIT;
objective.value = 2000 + (level - 1) * 400;
// Apply buff effects
if (roundBuff === "easyObjective") {
objective.value = Math.floor(objective.value * 0.8);
}
if (roundBuff === "extraMove") {
movesLeft += 3;
}
if (roundBuff === "comboStart") {
comboCount = 2;
comboTxt.setText('Combo X2');
}
if (roundBuff === "jellyClear") {
// Remove up to 3 random jellies
var jellies = [];
for (var i = 0; i < candies.length; ++i) {
if (candies[i].jelly) jellies.push(candies[i]);
}
for (var j = 0; j < 3 && jellies.length > 0; ++j) {
var idx = Math.floor(Math.random() * jellies.length);
var c = jellies[idx];
c.hideJelly();
c.jelly = false;
jellies.splice(idx, 1);
}
}
updateScore();
updateMoves();
levelTxt.setText('Level: ' + level);
if (levelTxt.style) {
levelTxt.style.stroke = 0x000000;
levelTxt.style.strokeThickness = 8;
}
objectiveTxt.setText('Target: ' + objective.value);
if (objectiveTxt.style) {
objectiveTxt.style.stroke = 0x000000;
objectiveTxt.style.strokeThickness = 8;
}
if (scoreTxt.style) {
scoreTxt.style.stroke = 0x000000;
scoreTxt.style.strokeThickness = 8;
}
if (movesTxt.style) {
movesTxt.style.stroke = 0x000000;
movesTxt.style.strokeThickness = 8;
}
if (comboTxt.style) {
comboTxt.style.stroke = 0x000000;
comboTxt.style.strokeThickness = 8;
}
createBoard();
canInput = true;
}
startGame();
modern mobile game background. In-Game asset. 2d. High contrast. No shadows
candy blue. In-Game asset. 2d. High contrast. No shadows
candy green. In-Game asset. 2d. High contrast. No shadows
candy orange. In-Game asset. 2d. High contrast. No shadows
candy purple. In-Game asset. 2d. High contrast. No shadows
candy red. In-Game asset. 2d. High contrast. No shadows
candy yellow. In-Game asset. 2d. High contrast. No shadows
candy bomb. In-Game asset. 2d. High contrast. No shadows
combo effect. In-Game asset. 2d. High contrast. No shadows
candy explosion. In-Game asset. 2d. High contrast. No shadows
explosion but not realistic. In-Game asset. 2d. High contrast. No shadows
jelly. In-Game asset. 2d. High contrast. No shadows