User prompt
jelly hala bir candynin içinde spawnlanıyor bunu engelle jelly tek başına spawnlanmalı
User prompt
jelly candylerin içinde spawn oluyor bunu engelle onuda bomb gibi sonradan spawn et
User prompt
ama hiç jelly spawnlanmıyor
User prompt
kartlar içinde image asset ekle
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading '4')' in or related to this line: 'var candy = board[pos.row][pos.col];' Line Number: 382
User prompt
ve bir mekanik ekleyeceğiz her level arttığında ekrana 3 kart çıkart ve bunlar buff versin rastgele seçtiğin kartın buffunu versin sadece o roundluk için
User prompt
ve bir mekanik ekleyeceğiz her level arttığında ekrana 3 kart çıkart ve bunlar buff versin rastgele sadece o roundluk için
User prompt
candy bombun gelme ihtimalini düşür
User prompt
anlamadın galiba candy bombun sürüklenme mekaniğini candyler gibi yap
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'stroke')' in or related to this line: 'levelTxt.style.stroke = 0x000000;' Line Number: 868
User prompt
bombun sürüklenme şeyi şeker gibi sadece sağa sol ve yukarı aşağıya sürüklenebilsin ve textlerin outline ı level artınca siliniyor bunu engelle tüm textlerde siyah outline kalacak
User prompt
candy bomb u bir anda spawnlama hani boşlukları doldurmak için candyler geliyorya he işte orada bir candy yerine bomb olsun
User prompt
arada candy bomb spawnla ve eğer yan yana uyumlu şekerler varsa onların yanına sürükleyince etrafındaki şekerler patlasın
User prompt
effect yazı olarak değilde image asset olarak çıksın
User prompt
ve şeker patlayınca ekrana efekt çıkar ve combo sistemi ekle ard arda şekerler patlarsa comboya girsin ve solda Combo X1 X2 olarak çıksın ve combo arttıkça çıkan efektlerde artısn
User prompt
Score ve movesinde text rengi veyaz ve texte siyah outline ekle
User prompt
her level arttığında target birazcık yükselsin ve şekerler ona göre balanced olsun ve Level ve target yazılarını boardın altına koy ve renklerini beyaz ve siyah outline yap texti
User prompt
oyuna sound assetleri ekle
User prompt
arka plan içinde bir image asset ekle
User prompt
şekerlerin üstünde kalıyor altında kalması lazım ve arkaplan rengini ten rengi yerine birazcık daha grimsi yap
User prompt
boardBg image assetini tam şekerlerin arkasını kaplayacak şekilde ayarla
Code edit (1 edits merged)
Please save this source code
User prompt
Candy Cascade Saga
Initial prompt
Create a full mobile puzzle game similar to Candy Crush Saga. The game should be a colorful and addictive match-3 puzzle game where players swap adjacent items on a grid to match 3 or more of the same type. The grid should be made up of colorful candies or gems with distinct shapes and colors. Game Mechanics: Players can swipe adjacent pieces to make horizontal or vertical matches of 3 or more. Matching 4 creates a special piece that clears a row or column. Matching 5 in a row creates a "color bomb" that clears all pieces of that color. L-shaped or T-shaped matches create an "explosive candy" that clears a 3x3 area. Cascading matches (combos) happen automatically when new pieces fall down. Each level has limited moves and specific objectives like "clear all jelly", "collect ingredients", or "reach a score target". Progression: The game should have a level map system (like a world map) where players unlock new stages as they win. Difficulty increases gradually with new obstacles such as chocolate blocks, licorice, locked tiles, and timed bombs. Include boosters such as lollipop hammer, extra moves, color switch, and others. Graphics and Style: Use a bright and cheerful cartoon style. Candies should look glossy and colorful. Smooth animations for swaps, matches, and explosions. Satisfying sound effects for matching and completing levels. UI / UX: Simple and intuitive drag-and-swap controls. A clear level objective screen before each level starts. In-game pause menu with options to retry or quit. Progress bar and move counter in-game. Other Features: Daily rewards system (coins, boosters, lives). A heart/life system: players lose a life on failure, regenerate over time. Option to connect with friends to send/receive lives. Include animations and particle effects for major combos or wins. Please make sure the gameplay is addictive, satisfying, and polished. Prioritize smooth feedback loops and game feel.
/**** * 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