Code edit (1 edits merged)
Please save this source code
User prompt
Candy Match Mania
Initial prompt
Create a Candy Crush-style match-3 puzzle game with a vibrant candy-themed design. The game should feature a grid of colorful candies (e.g., striped, wrapped, bomb, and rainbow variants) that players swap horizontally or vertically to match 3 or more of the same type. Include mechanics like level progression with increasing difficulty, unique objectives (e.g., clear jelly, collect specific candies, reach a target score), and obstacles (e.g., chocolate blockers, ice barriers, timed bombs). Add power-ups generated by matching 4+ candies (e.g., striped candy blasts, color bombs that clear all candies of a chosen color). Implement a lives system, daily rewards, and a boosters shop using in-game currency. Ensure smooth animations for candy swaps, matches, and explosions, along with cheerful sound effects and background music. The game must be mobile-responsive with touch controls and include a level editor for future content updates
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Candy class var Candy = Container.expand(function () { var self = Container.call(this); // Properties self.type = getRandomCandyType(); self.special = SPECIAL_NONE; self.blocker = BLOCKER_NONE; self.row = 0; self.col = 0; self.isFalling = false; self.isMatched = false; self.isSelected = false; self.asset = null; // Attach asset function updateAsset() { if (self.asset) { self.removeChild(self.asset); self.asset.destroy(); } var assetId = self.type; if (self.special === SPECIAL_STRIPED) assetId = 'candy_striped'; if (self.special === SPECIAL_BOMB) assetId = 'candy_bomb'; if (self.special === SPECIAL_RAINBOW) assetId = 'candy_rainbow'; if (self.blocker === BLOCKER_CHOCOLATE) assetId = 'blocker_chocolate'; if (self.blocker === BLOCKER_ICE) assetId = 'blocker_ice'; self.asset = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); if (self.isSelected) { self.asset.scaleX = 1.15; self.asset.scaleY = 1.15; } else { self.asset.scaleX = 1; self.asset.scaleY = 1; } } // Set type self.setType = function (type, special, blocker) { self.type = type || getRandomCandyType(); self.special = special || SPECIAL_NONE; self.blocker = blocker || BLOCKER_NONE; updateAsset(); }; // Set selected self.setSelected = function (selected) { self.isSelected = selected; updateAsset(); }; // Animate to position self.moveTo = function (x, y, duration, onFinish) { tween(self, { x: x, y: y }, { duration: duration || 200, easing: tween.easeInOut, onFinish: onFinish }); }; // Destroy self.destroyCandy = function () { if (self.asset) { self.removeChild(self.asset); self.asset.destroy(); self.asset = null; } self.destroy(); }; // Init updateAsset(); return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222244 }); /**** * Game Code ****/ // Fallback shapes for missing tracker icons (V/P/G/E/L/T) // Tracker icons (64x64) for each mechanical part // purple T-junction // gray locked pipe // green elbow // red gauge // orange pump // blue valve //{2.1} // Music and sounds // UI assets // Blocker assets // Candy assets // Prism button asset sized for large UI button (500x160 in use, so use 500x160 for best quality) // Candy and blocker assets sized to fit CELL_SIZE (200x200) for main board // Candy types // Music // Sounds // Blockers // Special candies // Candy shapes/colors // Board data var CANDY_TYPES = ['candy_red', 'candy_green', 'candy_blue', 'candy_yellow', 'candy_purple', 'candy_orange']; // Special types var SPECIAL_NONE = 0; var SPECIAL_STRIPED = 1; var SPECIAL_BOMB = 2; var SPECIAL_RAINBOW = 3; // Blocker types var BLOCKER_NONE = 0; var BLOCKER_CHOCOLATE = 1; var BLOCKER_ICE = 2; // Board size var BOARD_COLS = 7; var BOARD_ROWS = 9; var CELL_SIZE = 200; var BOARD_OFFSET_X = Math.floor((2048 - BOARD_COLS * CELL_SIZE) / 2); var BOARD_OFFSET_Y = 300; // Helper: get random candy type function getRandomCandyType() { return CANDY_TYPES[Math.floor(Math.random() * CANDY_TYPES.length)]; } // Helper: get random int function randInt(a, b) { return a + Math.floor(Math.random() * (b - a + 1)); } var board = []; var candies = []; var selectedCandy = null; var swapping = false; var animating = false; // --- Level Objective System --- // Objective types: 1) Collect Targets, 2) Clear Obstacles, 3) Rescue Items var OBJECTIVE_COLLECT = 1; var OBJECTIVE_CLEAR = 2; var OBJECTIVE_RESCUE = 3; // Mechanical asset mapping for objectives and icons var OBJECTIVE_ICONS = { valve: 'icon_valve_blue', pump: 'icon_pump_orange', gauge: 'icon_gauge_red', elbow: 'icon_elbow_green', locked: 'icon_locked_pipe', tjunction: 'icon_tjunction_purple', fallback: { valve: 'fallback_icon_V', pump: 'fallback_icon_P', gauge: 'fallback_icon_G', elbow: 'fallback_icon_E', locked: 'fallback_icon_L', tjunction: 'fallback_icon_T' } }; // Per-level objectives (mechanical theme, all asset-linked, but reduce new asset types and connect new assets to missions in different quantities) var levelObjectives = [ // Level 1: Collect 10 red candies (reuse candy_red asset) [{ type: OBJECTIVE_COLLECT, mech: 'valve', asset: 'candy_red', count: 10 }], // Level 2: Collect 10 green candies (reuse candy_green asset) [{ type: OBJECTIVE_COLLECT, mech: 'pump', asset: 'candy_green', count: 10 }], // Level 3: Collect 8 blue candies and 8 yellow candies (reuse candy_blue, candy_yellow) [{ type: OBJECTIVE_COLLECT, mech: 'gauge', asset: 'candy_blue', count: 8 }, { type: OBJECTIVE_COLLECT, mech: 'elbow', asset: 'candy_yellow', count: 8 }], // Level 4: Clear 6 chocolate blockers, collect 6 purple candies [{ type: OBJECTIVE_CLEAR, mech: 'locked', asset: 'blocker_chocolate', count: 6 }, { type: OBJECTIVE_COLLECT, mech: 'tjunction', asset: 'candy_purple', count: 6 }], // Level 5: Collect 7 orange candies, 7 blue candies [{ type: OBJECTIVE_COLLECT, mech: 'valve', asset: 'candy_orange', count: 7 }, { type: OBJECTIVE_COLLECT, mech: 'gauge', asset: 'candy_blue', count: 7 }], // Level 6: Clear 8 ice blockers, collect 5 green candies [{ type: OBJECTIVE_CLEAR, mech: 'locked', asset: 'blocker_ice', count: 8 }, { type: OBJECTIVE_COLLECT, mech: 'elbow', asset: 'candy_green', count: 5 }], // Level 7: Collect 5 striped candies, clear 5 chocolate blockers [{ type: OBJECTIVE_COLLECT, mech: 'tjunction', asset: 'candy_striped', count: 5 }, { type: OBJECTIVE_CLEAR, mech: 'locked', asset: 'blocker_chocolate', count: 5 }], // Level 8: Collect 4 bombs, 4 rainbow candies (specials) [{ type: OBJECTIVE_COLLECT, mech: 'valve', asset: 'candy_bomb', count: 4 }, { type: OBJECTIVE_COLLECT, mech: 'gauge', asset: 'candy_rainbow', count: 4 }], // Level 9: Clear 10 ice blockers, collect 3 of each color (red, green, blue, yellow, purple, orange) [{ type: OBJECTIVE_CLEAR, mech: 'locked', asset: 'blocker_ice', count: 10 }, { type: OBJECTIVE_COLLECT, mech: 'valve', asset: 'candy_red', count: 3 }, { type: OBJECTIVE_COLLECT, mech: 'pump', asset: 'candy_green', count: 3 }, { type: OBJECTIVE_COLLECT, mech: 'gauge', asset: 'candy_blue', count: 3 }, { type: OBJECTIVE_COLLECT, mech: 'elbow', asset: 'candy_yellow', count: 3 }, { type: OBJECTIVE_COLLECT, mech: 'tjunction', asset: 'candy_purple', count: 3 }, { type: OBJECTIVE_COLLECT, mech: 'valve', asset: 'candy_orange', count: 3 }] // ...repeat or randomize for higher levels, always reusing existing assets and connecting them to missions in different quantities ]; // For levels > 10, cycle or randomize objectives for demo function getObjectivesForLevel(level) { if (level < levelObjectives.length) return levelObjectives[level]; // For higher levels, mix and increase counts var base = levelObjectives[level % levelObjectives.length]; var newObj = []; for (var i = 0; i < base.length; ++i) { var o = {}; for (var k in base[i]) o[k] = base[i][k]; o.count = Math.ceil(o.count * (1 + level / 20)); newObj.push(o); } return newObj; } // Track progress for current objectives var currentObjectives = []; var currentObjectiveProgress = []; // Progress panel UI var progressPanel = new Container(); LK.gui.top.addChild(progressPanel); progressPanel.x = 0; progressPanel.y = 350; progressPanel.visible = false; // Process tracker panel (shows number of tasks performed and left, links to assets) var processTrackerPanel = new Container(); LK.gui.top.addChild(processTrackerPanel); processTrackerPanel.x = 0; processTrackerPanel.y = 520; processTrackerPanel.visible = false; // Star bonus UI var starPanel = new Container(); LK.gui.top.addChild(starPanel); starPanel.x = 0; starPanel.y = 420; starPanel.visible = false; var starIcons = []; for (var i = 0; i < 3; ++i) { var star = LK.getAsset('candy_rainbow', { anchorX: 0.5, anchorY: 0.5, x: 80 * i, y: 0, width: 60, height: 60, alpha: 0.5 }); starPanel.addChild(star); starIcons.push(star); } // Helper: update progress panel function updateProgressPanel() { // Remove old children while (progressPanel.children && progressPanel.children.length > 0) { var ch = progressPanel.children.pop(); ch.destroy && ch.destroy(); } // For each objective, show icon, progress, and animation if completed for (var i = 0; i < currentObjectives.length; ++i) { var obj = currentObjectives[i]; var iconId = null; // Use asset icon for collect/clear, fallback to mechanical icon if ((obj.type === OBJECTIVE_COLLECT || obj.type === OBJECTIVE_CLEAR) && obj.asset) { iconId = obj.asset; } else if (OBJECTIVE_ICONS[obj.mech]) { iconId = OBJECTIVE_ICONS[obj.mech]; } else if (OBJECTIVE_ICONS.fallback[obj.mech]) { iconId = OBJECTIVE_ICONS.fallback[obj.mech]; } else { iconId = OBJECTIVE_ICONS.fallback.valve; // fallback to V } var icon = LK.getAsset(iconId, { anchorX: 0.5, anchorY: 0.5, x: 80 + i * 320, y: 0, width: 90, height: 90 }); progressPanel.addChild(icon); var txt = new Text2('', { size: 60, fill: "#fff" }); txt.anchor.set(0, 0.5); txt.x = 140 + i * 320; txt.y = 0; var val = currentObjectiveProgress[i]; var goal = obj.count; txt.setText(val + " / " + goal); progressPanel.addChild(txt); // If completed, flash icon if (val >= goal) { LK.effects.flashObject(icon, 0x00ff00, 600); } } } // --- Process Tracker Panel: show number of tasks performed and left, link to assets --- while (processTrackerPanel.children && processTrackerPanel.children.length > 0) { var ch = processTrackerPanel.children.pop(); ch.destroy && ch.destroy(); } // Count total and completed tasks var totalTasks = currentObjectives.length; var completedTasks = 0; for (var i = 0; i < currentObjectives.length; ++i) { if (currentObjectiveProgress[i] >= currentObjectives[i].count) completedTasks++; } // Show summary text var summaryTxt = new Text2("Tasks: " + completedTasks + " / " + totalTasks, { size: 54, fill: "#fff" }); summaryTxt.anchor.set(0, 0.5); summaryTxt.x = 0; summaryTxt.y = 0; processTrackerPanel.addChild(summaryTxt); // For each task, show icon and progress for (var i = 0; i < currentObjectives.length; ++i) { var obj = currentObjectives[i]; var iconId = null; if ((obj.type === OBJECTIVE_COLLECT || obj.type === OBJECTIVE_CLEAR) && obj.asset) { iconId = obj.asset; } else if (obj.type === OBJECTIVE_RESCUE && obj.asset) { iconId = obj.asset; } else if (OBJECTIVE_ICONS[obj.mech]) { iconId = OBJECTIVE_ICONS[obj.mech]; } else if (OBJECTIVE_ICONS.fallback[obj.mech]) { iconId = OBJECTIVE_ICONS.fallback[obj.mech]; } else { iconId = OBJECTIVE_ICONS.fallback.valve; } var icon = LK.getAsset(iconId, { anchorX: 0.5, anchorY: 0.5, x: 220 + i * 320, y: 0, width: 70, height: 70 }); processTrackerPanel.addChild(icon); var txt = new Text2(currentObjectiveProgress[i] + " / " + obj.count, { size: 44, fill: "#fff" }); txt.anchor.set(0, 0.5); txt.x = 260 + i * 320; txt.y = 0; processTrackerPanel.addChild(txt); // If completed, flash icon if (currentObjectiveProgress[i] >= obj.count) { LK.effects.flashObject(icon, 0x00ff00, 600); } } // Show/hide process tracker panel based on game state processTrackerPanel.visible = progressPanel.visible; // Helper: update star panel function updateStarPanel() { var percent = movesLeft / levels[currentLevel].moves; for (var i = 0; i < 3; ++i) { starIcons[i].alpha = 0.3; } if (percent > 0.5) { starIcons[0].alpha = 1; starIcons[1].alpha = 1; starIcons[2].alpha = 1; } else if (percent > 0.25) { starIcons[0].alpha = 1; starIcons[1].alpha = 1; } else if (percent > 0) { starIcons[0].alpha = 1; } } // Helper: check if all objectives are complete function allObjectivesComplete() { for (var i = 0; i < currentObjectives.length; ++i) { if (currentObjectiveProgress[i] < currentObjectives[i].count) return false; } return true; } // --- Timed Bombs and Conveyor Belts (mechanics) --- var timedBombs = []; // {candy, movesLeft} var conveyorActive = false; // Add a bomb to a random candy (for demo, add on level 4+) function addTimedBomb() { var candidates = []; for (var i = 0; i < candies.length; ++i) { var c = candies[i]; if (c.blocker === BLOCKER_NONE && c.special === SPECIAL_NONE) candidates.push(c); } if (candidates.length > 0) { var c = candidates[randInt(0, candidates.length - 1)]; c.special = SPECIAL_BOMB; c.setType(c.type, SPECIAL_BOMB, c.blocker); timedBombs.push({ candy: c, movesLeft: 3 }); } } // Conveyor: shift all candies right by 1 (for demo, level 6+) function shiftConveyor() { for (var row = 0; row < BOARD_ROWS; ++row) { var last = board[row][BOARD_COLS - 1]; for (var col = BOARD_COLS - 1; col > 0; --col) { board[row][col] = board[row][col - 1]; board[row][col].col = col; board[row][col].moveTo(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 180); } board[row][0] = last; last.col = 0; last.moveTo(0 * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 180); } } // --- Per-level winCondition helpers --- // Returns true if all blockers (chocolate/ice) are cleared from the board function allBlockersCleared() { for (var row = 0; row < BOARD_ROWS; ++row) { for (var col = 0; col < BOARD_COLS; ++col) { var c = board[row][col]; // Only count blockers that are visible and not matched if (c && c.visible !== false && c.isMatched !== true && (c.blocker === BLOCKER_CHOCOLATE || c.blocker === BLOCKER_ICE)) { // For ice, also check if it's not just cracked (must be fully removed) if (c.blocker === BLOCKER_ICE && !c._iceCracked) { return false; } if (c.blocker === BLOCKER_CHOCOLATE) { return false; } } } } return true; } // Returns true if all candies are cleared (for e.g. 'explode all boxes' levels) function allBoxesCleared() { for (var row = 0; row < BOARD_ROWS; ++row) { for (var col = 0; col < BOARD_COLS; ++col) { var c = board[row][col]; // If any visible, non-matched, non-blocker candy remains, not cleared if (c && c.visible !== false && c.isMatched !== true && c.blocker === BLOCKER_NONE) { return false; } } } return true; } // Example: fun feature - birds that must reach the bottom and be freed // Returns true if all birds are at the bottom row and visible function allBirdsFreed() { for (var i = 0; i < candies.length; ++i) { var c = candies[i]; if (c.isBird && c.row !== BOARD_ROWS - 1) { return false; } } return true; } // --- Level definitions: 100 levels, each with unique win condition and fixed moves --- // Level 1 is the easiest, level 100 is the hardest var levels = []; // Moves curve: tuned for difficulty and level progression // Level 1-2: very easy, more moves; 3-5: easy; 6-10: normal; 11-20: moderate; 21-30: challenging; 31-50: hard; 51-100: expert var movesCurve = [25, 22, // 1-2: very easy 18, 17, 16, // 3-5: easy 15, 15, 14, 14, 13, // 6-10: normal 13, 13, 12, 12, 12, 11, 11, 11, 11, 11, // 11-20: moderate 10, 10, 10, 10, 10, 9, 9, 9, 9, 9, // 21-30: challenging 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 31-40: hard 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // 41-50: hard 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, // 51-60: expert 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, // 61-70: expert 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, // 71-80: expert 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 // 81-90: expert ]; // Fill up to 100 levels with 5 moves for 91-100 while (movesCurve.length < 100) movesCurve.push(5); for (var i = 0; i < 100; ++i) { var level = {}; // Difficulty curve: more blockers, less moves, higher targets as level increases if (i === 0) { // Level 1: Easiest, no blockers level.moves = movesCurve[i]; level.target = 400; level.blockers = []; level.description = "Explode all boxes!\nClear the board to win.\nNo blockers."; } else if (i === 1) { // Level 2: Chocolate row level.moves = movesCurve[i]; level.target = 600; level.blockers = [{ row: 4, type: BLOCKER_CHOCOLATE }]; level.description = "Clear all chocolate!\nRemove every blocker to win."; } else if (i === 2) { // Level 3: Ice at bottom level.moves = movesCurve[i]; level.target = 800; level.blockers = [{ row: 7, type: BLOCKER_ICE }, { row: 8, type: BLOCKER_ICE }]; level.description = "Break all the ice!\nClear every blocker to win."; } else if (i === 3) { // Level 4: Zig-zag blockers level.moves = movesCurve[i]; level.target = 1000; level.blockers = [{ row: 2, type: BLOCKER_CHOCOLATE }, { row: 3, type: BLOCKER_ICE }, { row: 4, type: BLOCKER_CHOCOLATE }, { row: 5, type: BLOCKER_ICE }]; level.description = "Clear all blockers!\nZig-zag pattern challenge."; } else if (i === 4) { // Level 5: Score challenge level.moves = movesCurve[i]; level.target = 1400; level.blockers = [{ row: 2, type: BLOCKER_CHOCOLATE }, { row: 3, type: BLOCKER_ICE }, { row: 4, type: BLOCKER_CHOCOLATE }, { row: 5, type: BLOCKER_ICE }, { row: 6, type: BLOCKER_CHOCOLATE }]; level.description = "Score challenge!\nReach the target score to win."; } else if (i === 5) { // Level 6: Cross/diamond blockers level.moves = movesCurve[i]; level.target = 1700; level.blockers = [{ row: 0, type: BLOCKER_CHOCOLATE }, { row: 4, type: BLOCKER_ICE }, { row: 8, type: BLOCKER_CHOCOLATE }]; level.description = "Break all blockers!\nCross and diamond pattern."; } else if (i === 6) { // Level 7: U-shape blockers level.moves = movesCurve[i]; level.target = 1900; level.blockers = [{ row: 0, type: BLOCKER_CHOCOLATE }, { row: 8, type: BLOCKER_CHOCOLATE }, { row: 4, type: BLOCKER_ICE }, { row: 7, type: BLOCKER_ICE }]; level.description = "Clear all blockers!\nU-shape at the bottom."; } else if (i === 7) { // Level 8: Vertical stripe blockers level.moves = movesCurve[i]; level.target = 2100; level.blockers = [{ row: 1, type: BLOCKER_ICE }, { row: 3, type: BLOCKER_CHOCOLATE }, { row: 5, type: BLOCKER_ICE }, { row: 7, type: BLOCKER_CHOCOLATE }]; level.description = "Clear all blockers!\nVertical stripe in the center."; } else if (i === 8) { // Level 9: Spiral blockers level.moves = movesCurve[i]; level.target = 2400; level.blockers = [{ row: 0, type: BLOCKER_ICE }, { row: 1, type: BLOCKER_CHOCOLATE }, { row: 2, type: BLOCKER_ICE }, { row: 3, type: BLOCKER_CHOCOLATE }, { row: 4, type: BLOCKER_ICE }, { row: 5, type: BLOCKER_CHOCOLATE }, { row: 6, type: BLOCKER_ICE }, { row: 7, type: BLOCKER_CHOCOLATE }]; level.description = "Clear all blockers!\nSpiral pattern."; } else if (i === 9) { // Level 10: Boss level.moves = movesCurve[i]; level.target = 3000; level.blockers = [{ row: 0, type: BLOCKER_CHOCOLATE }, { row: 1, type: BLOCKER_ICE }, { row: 2, type: BLOCKER_CHOCOLATE }, { row: 3, type: BLOCKER_ICE }, { row: 4, type: BLOCKER_CHOCOLATE }, { row: 5, type: BLOCKER_ICE }, { row: 6, type: BLOCKER_CHOCOLATE }, { row: 7, type: BLOCKER_ICE }, { row: 8, type: BLOCKER_CHOCOLATE }]; level.description = "Boss Level!\nReach the target score to win."; } else { // Levels 11-100: Increase difficulty, alternate win conditions, more blockers, less moves, higher targets var base = levels[i % 10]; // Use movesCurve for main progression, but allow special bumps below level.moves = movesCurve[i]; level.target = base.target + 400 * i; // Copy blockers and add more for higher levels level.blockers = []; for (var b = 0; b < base.blockers.length; ++b) { level.blockers.push({ row: base.blockers[b].row, type: base.blockers[b].type }); } // Every 10th level: add extra blockers if (i > 0 && i % 10 === 0) { for (var r = 0; r < 3; ++r) { level.blockers.push({ row: r, type: i % 20 === 0 ? BLOCKER_CHOCOLATE : BLOCKER_ICE }); } level.description = "Super challenge!\nExtra blockers added.\nCan you win?"; // Give a small moves bump for every 10th level level.moves += 1; } else if (i > 0 && i % 5 === 0) { // Every 5th level: Boss, higher target level.target += 1000; level.description = "Boss Level!\nBlockers everywhere.\nShow your skills!"; // Give a moves bump for boss level.moves += 1; } else if (i > 0 && i % 25 === 0) { // Every 25th: Mega Boss level.target += 2000; level.moves = Math.max(5, level.moves - 2); level.description = "Mega Boss!\nThe ultimate test.\nGood luck!"; } else if (i > 0 && i % 3 === 0) { // Every 3rd: add random blocker row var extraRow = randInt(0, BOARD_ROWS - 1); var extraType = Math.random() < 0.5 ? BLOCKER_CHOCOLATE : BLOCKER_ICE; level.blockers.push({ row: extraRow, type: extraType }); level.description = "Surprise row!\nExtra blockers appear.\nKeep matching!"; } else if (i > 0 && i % 7 === 0) { // Every 7th: add two random blockers for (var r = 0; r < 2; ++r) { var randRow = randInt(0, BOARD_ROWS - 1); var randType = Math.random() < 0.5 ? BLOCKER_CHOCOLATE : BLOCKER_ICE; level.blockers.push({ row: randRow, type: randType }); } level.description = "Blocker surprise!\nRandom blockers added.\nStay sharp!"; } else if (i > 0 && i % 13 === 0) { // Every 13th: Lucky, more moves, higher target level.moves += 3; level.target += 1500; level.description = "Lucky Level!\nMore moves, higher target.\nGo for it!"; } else { // Default: inherit description level.description = base.description; } // No winCondition property at all } levels.push(level); } // Always start at level 1 for every player/session storage.currentLevel = 0; var currentLevel = 0; var movesLeft = typeof storage.movesLeft !== "undefined" ? storage.movesLeft : levels[currentLevel].moves; var targetScore = levels[currentLevel].target; var score = typeof storage.score !== "undefined" ? storage.score : 0; var scoreTxt = null; var movesTxt = null; var targetTxt = null; var boardContainer = null; var matchQueue = []; var refillQueue = []; var isProcessing = false; // --- Start Screen Overlay --- var startScreen = new Container(); LK.gui.center.addChild(startScreen); startScreen.visible = true; // Background overlay (semi-transparent) var startBg = LK.getAsset('blocker_ice', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, width: 1200, height: 1600, alpha: 0.85 }); startScreen.addChild(startBg); // Game title var titleTxt = new Text2('Candy Match Saga', { size: 180, fill: "#fff" }); titleTxt.anchor.set(0.5, 0.5); titleTxt.x = 0; titleTxt.y = -400; startScreen.addChild(titleTxt); // Subtitle var subtitleTxt = new Text2('Match candies, beat levels, have fun!', { size: 70, fill: "#fff" }); subtitleTxt.anchor.set(0.5, 0.5); subtitleTxt.x = 0; subtitleTxt.y = -250; startScreen.addChild(subtitleTxt); // Play button var playBtn = new Container(); var playBtnBg = LK.getAsset('start_button', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, width: 500, height: 160 }); playBtn.addChild(playBtnBg); // Removed the 'Start' label from the start screen play button playBtn.anchorX = 0.5; playBtn.anchorY = 0.5; playBtn.x = 0; playBtn.y = 200; playBtn.interactive = true; playBtn.buttonMode = true; playBtn.down = function (x, y, obj) { startScreen.visible = false; // Show game UI scoreTxt.visible = true; movesTxt.visible = true; targetTxt.visible = true; levelTopRightTxt.visible = true; bonusPanel.visible = true; boardContainer.visible = true; if (levelTxt) levelTxt.visible = true; if (prevLevelBtn) prevLevelBtn.visible = true; if (resetBtn) resetBtn.visible = true; // Removed reference to nextLevelBtn which is not defined if (window.levelDescTxt) window.levelDescTxt.visible = true; if (counterPanel) counterPanel.visible = true; }; startScreen.addChild(playBtn); // Hide game UI until play is pressed // GUI scoreTxt = new Text2('Score: 0', { size: 90, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); scoreTxt.visible = false; movesTxt = new Text2('Moves: 20', { size: 70, fill: "#fff" }); movesTxt.anchor.set(0.5, 0); LK.gui.top.addChild(movesTxt); movesTxt.y = 110; movesTxt.visible = false; targetTxt = new Text2('Target: 5000', { size: 60, fill: "#fff" }); targetTxt.anchor.set(0.5, 0); LK.gui.top.addChild(targetTxt); targetTxt.y = 180; targetTxt.visible = false; // Add level label to top right var levelTopRightTxt = new Text2('Level: 1', { size: 70, fill: "#fff" }); levelTopRightTxt.anchor.set(1, 0); LK.gui.topRight.addChild(levelTopRightTxt); levelTopRightTxt.x = 0; levelTopRightTxt.y = 0; levelTopRightTxt.visible = false; // --- Bonus UI --- var bonusPanel = new Container(); LK.gui.top.addChild(bonusPanel); bonusPanel.x = 0; bonusPanel.y = 250; bonusPanel.visible = false; // Track which bonus is selected (null, "bomb", etc) var selectedBonus = null; // Bomb bonus button var bombBtnBg = LK.getAsset('candy_bomb', { anchorX: 0.5, anchorY: 0.5, x: 120, y: 0, width: 140, height: 140 }); bonusPanel.addChild(bombBtnBg); var bombBtnLabel = new Text2('Bomb', { size: 48, fill: "#fff" }); bombBtnLabel.anchor.set(0.5, 0); bombBtnLabel.x = 120; bombBtnLabel.y = 80; bonusPanel.addChild(bombBtnLabel); bombBtnBg.interactive = true; bombBtnBg.buttonMode = true; bombBtnBg.down = function (x, y, obj) { selectedBonus = selectedBonus === "bomb" ? null : "bomb"; // Visual feedback bombBtnBg.scaleX = bombBtnBg.scaleY = selectedBonus === "bomb" ? 1.2 : 1.0; }; // Optionally, add more bonuses here in the future // Board container boardContainer = new Container(); game.addChild(boardContainer); boardContainer.x = BOARD_OFFSET_X; boardContainer.y = BOARD_OFFSET_Y; boardContainer.visible = false; // Initialize board function initBoard() { // Clear previous for (var i = 0; i < candies.length; ++i) { if (candies[i]) candies[i].destroyCandy(); } candies = []; board = []; // Initialize objectives for this level currentObjectives = getObjectivesForLevel(currentLevel); currentObjectiveProgress = []; for (var i = 0; i < currentObjectives.length; ++i) currentObjectiveProgress[i] = 0; progressPanel.visible = true; updateProgressPanel(); starPanel.visible = true; updateStarPanel(); updateCounterPanel(); timedBombs = []; conveyorActive = currentLevel >= 5; // Enable conveyor on level 6+ // --- Ensure all required assets for objectives are present in the board --- // Build a list of required asset counts for this level's objectives var requiredAssets = {}; for (var i = 0; i < currentObjectives.length; ++i) { var obj = currentObjectives[i]; // Only count collect/clear objectives for mechanical assets if (obj.asset) { requiredAssets[obj.asset] = (requiredAssets[obj.asset] || 0) + obj.count; } } // Track how many of each asset we have placed var placedAssets = {}; for (var k in requiredAssets) placedAssets[k] = 0; for (var row = 0; row < BOARD_ROWS; ++row) { board[row] = []; for (var col = 0; col < BOARD_COLS; ++col) { var candy = new Candy(); candy.row = row; candy.col = col; candy.x = col * CELL_SIZE + CELL_SIZE / 2; candy.y = row * CELL_SIZE + CELL_SIZE / 2; // Unique blocker logic per level var blockers = levels[currentLevel].blockers; var placedBlocker = false; // Guarantee at least one open path per row for passability // We'll use a random openCol for each row, but keep it consistent for the row var openCol = -1; if (blockers.length > 0) { // For each row, pick a random open column (or center for symmetry) openCol = Math.floor(BOARD_COLS / 2); // For more variety, you could use: openCol = randInt(0, BOARD_COLS - 1); } for (var b = 0; b < blockers.length; ++b) { if (row === blockers[b].row) { // Level-specific patterns // Level 5/10: checkerboard, but add a diagonal line of blockers for extra challenge if ((currentLevel === 4 || currentLevel === 9) && ((row + col) % 2 === 0 || row === col)) { // Always leave openCol open if (col !== openCol) { candy.setType(candy.type, SPECIAL_NONE, blockers[b].type); placedBlocker = true; } } // Level 6: cross pattern, but add a diamond shape in the center else if (currentLevel === 5 && (col === Math.floor(BOARD_COLS / 2) || row === Math.floor(BOARD_ROWS / 2) || Math.abs(col - Math.floor(BOARD_COLS / 2)) === Math.abs(row - Math.floor(BOARD_ROWS / 2)) && Math.abs(col - Math.floor(BOARD_COLS / 2)) <= 2)) { if (col !== openCol) { candy.setType(candy.type, SPECIAL_NONE, blockers[b].type); placedBlocker = true; } } // Level 7: chocolate on sides, ice in center, but add blockers in a U shape at the bottom else if (currentLevel === 6 && ((row === 0 || row === BOARD_ROWS - 1) && blockers[b].type === BLOCKER_CHOCOLATE && (col === 0 || col === BOARD_COLS - 1) || row >= BOARD_ROWS - 3 && (col === 0 || col === BOARD_COLS - 1 || row === BOARD_ROWS - 1 && col > 0 && col < BOARD_COLS - 1))) { if (col !== openCol) { candy.setType(candy.type, SPECIAL_NONE, blockers[b].type); placedBlocker = true; } } // Level 8: alternating rows, but add a vertical stripe in the center else if (currentLevel === 7 && (row % 2 === b % 2 || col === Math.floor(BOARD_COLS / 2))) { if (col !== openCol) { candy.setType(candy.type, SPECIAL_NONE, blockers[b].type); placedBlocker = true; } } // Level 9: spiral (simulate with increasing rows), but add a spiral arm from the bottom right else if (currentLevel === 8 && (col >= row && col < BOARD_COLS - row || row + col === BOARD_COLS + BOARD_ROWS - 2 - row && row > BOARD_ROWS / 2)) { if (col !== openCol) { candy.setType(candy.type, SPECIAL_NONE, blockers[b].type); placedBlocker = true; } } // Level 10: nearly every row blocked, but leave a zig-zag path open else if (currentLevel === 9 && !((row + col) % 2 === 1 && col !== 0 && col !== BOARD_COLS - 1)) { if (col !== openCol) { candy.setType(candy.type, SPECIAL_NONE, blockers[b].type); placedBlocker = true; } } // Default: alternate blockers on even/odd columns for variety, but add a random chance for double blockers else if (!placedBlocker && (blockers[b].type === BLOCKER_CHOCOLATE && col % 2 === 0 || blockers[b].type === BLOCKER_ICE && col % 2 === 1 || Math.random() < 0.07)) { if (col !== openCol) { candy.setType(candy.type, SPECIAL_NONE, blockers[b].type); placedBlocker = true; } } } } // --- Place required assets for objectives --- // Only place if not a blocker if (!placedBlocker) { // Try to place required assets first, if not enough placed yet var placed = false; for (var assetName in requiredAssets) { if (placedAssets[assetName] < requiredAssets[assetName]) { // Place this asset as a candy type candy.setType(assetName, SPECIAL_NONE, BLOCKER_NONE); placedAssets[assetName]++; placed = true; break; } } // If all required assets are placed, continue to spawn them randomly if they are part of objectives if (!placed) { // Build a list of all asset names that are required for objectives in this level var objectiveAssetNames = []; for (var assetName in requiredAssets) { objectiveAssetNames.push(assetName); } // --- Set random proportions for each objective asset for this level --- // Only generate once per board init if (!window._objectiveAssetProportions || window._objectiveAssetProportionsLevel !== currentLevel) { window._objectiveAssetProportions = {}; var total = 0; for (var i = 0; i < objectiveAssetNames.length; ++i) { // Give each asset a random weight between 1 and 100 var w = randInt(1, 100); window._objectiveAssetProportions[objectiveAssetNames[i]] = w; total += w; } // Normalize to probabilities (sum to 1) for (var i = 0; i < objectiveAssetNames.length; ++i) { var k = objectiveAssetNames[i]; window._objectiveAssetProportions[k] /= total; } window._objectiveAssetProportionsLevel = currentLevel; } // If there are any objective assets, pick one to spawn according to the random proportions // Reduce probability to 25% to lower chance of self-exploding objective boxes if (objectiveAssetNames.length > 0 && Math.random() < 0.25) { // 25% chance to spawn an objective asset // Weighted random pick var r = Math.random(); var acc = 0; var chosenAsset = objectiveAssetNames[0]; for (var i = 0; i < objectiveAssetNames.length; ++i) { var k = objectiveAssetNames[i]; acc += window._objectiveAssetProportions[k]; if (r <= acc) { chosenAsset = k; break; } } candy.setType(chosenAsset, SPECIAL_NONE, BLOCKER_NONE); } else { // Otherwise, use a random candy type candy.setType(getRandomCandyType(), SPECIAL_NONE, BLOCKER_NONE); } } } // Always add candies to the top of the boardContainer to ensure correct z-ordering boardContainer.addChild(candy); if (boardContainer.children && boardContainer.children.length > 1) { // Move the newly added candy to the top (end of children array) boardContainer.removeChild(candy); boardContainer.addChild(candy); } candies.push(candy); board[row][col] = candy; } } // Remove initial matches removeInitialMatches(); // Ensure no 3-in-a-row/col at board creation for (var row = 0; row < BOARD_ROWS; ++row) { //{aU.1} for (var col = 0; col < BOARD_COLS; ++col) { //{aU.2} // Only check non-blockers if (board[row][col].blocker !== BLOCKER_NONE) continue; //{aU.3} var forbiddenTypes = {}; // Check left if (col >= 2 && board[row][col - 1].type === board[row][col - 2].type) { forbiddenTypes[board[row][col - 1].type] = true; //{aU.4} } // Check up if (row >= 2 && board[row - 1][col].type === board[row - 2][col].type) { forbiddenTypes[board[row - 1][col].type] = true; //{aU.5} } // If current type is forbidden, reroll var tries = 0; while (forbiddenTypes[board[row][col].type] && tries < 10) { //{aU.6} var newType = getRandomCandyType(); // Avoid forbidden types while (forbiddenTypes[newType] && tries < 10) { newType = getRandomCandyType(); tries++; } board[row][col].setType(newType); tries++; } } //{aU.7} } //{aU.8} } // Remove initial matches to avoid auto-matches at start function removeInitialMatches() { for (var row = 0; row < BOARD_ROWS; ++row) { for (var col = 0; col < BOARD_COLS; ++col) { // Only check non-blockers if (board[row][col].blocker !== BLOCKER_NONE) continue; //{aW.1} var changed = false; var tries = 0; do { var type = board[row][col].type; var hasRowMatch = col >= 2 && board[row][col - 1].type === type && board[row][col - 2].type === type; var hasColMatch = row >= 2 && board[row - 1][col].type === type && board[row - 2][col].type === type; if (hasRowMatch || hasColMatch) { var forbiddenTypes = {}; if (hasRowMatch) forbiddenTypes[board[row][col - 1].type] = true; if (hasColMatch) forbiddenTypes[board[row - 1][col].type] = true; var newType = getRandomCandyType(); while ((forbiddenTypes[newType] || newType === type) && tries < 10) { newType = getRandomCandyType(); tries++; } board[row][col].setType(newType); changed = true; } else { changed = false; } tries++; } while (changed && tries < 10); } } } // Get candy at board position function getCandyAt(row, col) { if (row < 0 || row >= BOARD_ROWS || col < 0 || col >= BOARD_COLS) return null; return board[row][col]; } // Swap two candies function swapCandies(c1, c2, cb) { swapping = true; var r1 = c1.row, c1c = c1.col, r2 = c2.row, c2c = c2.col; // Swap in board board[r1][c1c] = c2; board[r2][c2c] = c1; // Swap row/col var tmpRow = c1.row, tmpCol = c1.col; c1.row = r2; c1.col = c2c; c2.row = r1; c2.col = c1c; // Ensure board and candies are in sync after swap board[c1.row][c1.col] = c1; board[c2.row][c2.col] = c2; // Animate var done = 0; c1.moveTo(c1.col * CELL_SIZE + CELL_SIZE / 2, c1.row * CELL_SIZE + CELL_SIZE / 2, 180, function () { done++; if (done === 2 && cb) { swapping = false; cb(); } }); c2.moveTo(c2.col * CELL_SIZE + CELL_SIZE / 2, c2.row * CELL_SIZE + CELL_SIZE / 2, 180, function () { done++; if (done === 2 && cb) { swapping = false; cb(); } }); LK.getSound('swap').play(); } // Check if two candies are adjacent function areAdjacent(c1, c2) { var dr = Math.abs(c1.row - c2.row); var dc = Math.abs(c1.col - c2.col); // Allow swapping in all four directions (up, down, left, right) return dr === 1 && dc === 0 || dr === 0 && dc === 1; } // Find all matches on the board, including special candy creation function findMatches() { var matches = []; var matchGroups = []; // For special candy creation // Horizontal for (var row = 0; row < BOARD_ROWS; ++row) { var count = 1; var startCol = 0; for (var col = 1; col < BOARD_COLS; ++col) { var prev = board[row][col - 1]; var curr = board[row][col]; if (curr.type === prev.type && curr.blocker === BLOCKER_NONE && prev.blocker === BLOCKER_NONE) { count++; } else { if (count >= 3) { var group = []; for (var k = 0; k < count; ++k) { group.push(board[row][col - 1 - k]); } matches = matches.concat(group); matchGroups.push(group); } count = 1; startCol = col; } } if (count >= 3) { var group = []; for (var k = 0; k < count; ++k) { group.push(board[row][BOARD_COLS - 1 - k]); } matches = matches.concat(group); matchGroups.push(group); } } // Vertical for (var col = 0; col < BOARD_COLS; ++col) { var count = 1; var startRow = 0; for (var row = 1; row < BOARD_ROWS; ++row) { var prev = board[row - 1][col]; var curr = board[row][col]; if (curr.type === prev.type && curr.blocker === BLOCKER_NONE && prev.blocker === BLOCKER_NONE) { count++; } else { if (count >= 3) { var group = []; for (var k = 0; k < count; ++k) { group.push(board[row - 1 - k][col]); } matches = matches.concat(group); matchGroups.push(group); } count = 1; startRow = row; } } if (count >= 3) { var group = []; for (var k = 0; k < count; ++k) { group.push(board[BOARD_ROWS - 1 - k][col]); } matches = matches.concat(group); matchGroups.push(group); } } // Detect 2x2 square matches and mark for bomb for (var row = 0; row < BOARD_ROWS - 1; ++row) { for (var col = 0; col < BOARD_COLS - 1; ++col) { var c1 = board[row][col]; var c2 = board[row][col + 1]; var c3 = board[row + 1][col]; var c4 = board[row + 1][col + 1]; if (c1.type === c2.type && c1.type === c3.type && c1.type === c4.type && c1.blocker === BLOCKER_NONE && c2.blocker === BLOCKER_NONE && c3.blocker === BLOCKER_NONE && c4.blocker === BLOCKER_NONE) { // Only add if not already in a match group for this square var alreadyMatched = false; for (var mg = 0; mg < matchGroups.length; ++mg) { var g = matchGroups[mg]; if (g.indexOf(c1) !== -1 && g.indexOf(c2) !== -1 && g.indexOf(c3) !== -1 && g.indexOf(c4) !== -1) { alreadyMatched = true; break; } } if (!alreadyMatched) { // Mark the top-left as bomb, others as normal c1.special = SPECIAL_BOMB; c1.setType(c1.type, SPECIAL_BOMB, c1.blocker); // Remove c1 from matches so it is not destroyed, but destroy the other 3 var group = [c2, c3, c4]; matches = matches.concat(group); matchGroups.push([c1, c2, c3, c4]); // Store a flag for c1 to trigger a special 2x2 bomb explosion in removeMatches c1._pendingSquareBomb = true; } } } } // Remove duplicates var unique = []; for (var i = 0; i < matches.length; ++i) { if (unique.indexOf(matches[i]) === -1) unique.push(matches[i]); } // Mark special candy creation (striped, bomb, rainbow) for (var g = 0; g < matchGroups.length; ++g) { var group = matchGroups[g]; if (group.length === 4) { // Striped candy: horizontal or vertical var isHorizontal = group[0].row === group[1].row; var specialCandy = group[1]; // Place special at second in group if (specialCandy.special === SPECIAL_NONE) { specialCandy.special = SPECIAL_STRIPED; specialCandy.setType(specialCandy.type, SPECIAL_STRIPED, specialCandy.blocker); // Mark orientation for correct effect specialCandy._stripedOrientation = isHorizontal ? "horizontal" : "vertical"; } // Remove the special candy from the group so it is not destroyed for (var i = 0; i < group.length; ++i) { if (group[i] === specialCandy) { group.splice(i, 1); break; } } } if (group.length === 5) { // Color bomb (rainbow) only for exactly 5-in-a-row var specialCandy = group[Math.floor(group.length / 2)]; // Place in the middle if (specialCandy.special === SPECIAL_NONE) { specialCandy.special = SPECIAL_RAINBOW; specialCandy.setType(specialCandy.type, SPECIAL_RAINBOW, specialCandy.blocker); } // Remove the special candy from the group so it is not destroyed for (var i = 0; i < group.length; ++i) { if (group[i] === specialCandy) { group.splice(i, 1); break; } } } } // Bomb candy for T or L shape // Find intersections of horizontal and vertical matches for (var i = 0; i < matchGroups.length; ++i) { var groupA = matchGroups[i]; if (groupA.length !== 3) continue; for (var j = i + 1; j < matchGroups.length; ++j) { var groupB = matchGroups[j]; if (groupB.length !== 3) continue; // Check for intersection for (var a = 0; a < 3; ++a) { for (var b = 0; b < 3; ++b) { if (groupA[a] === groupB[b]) { // Place bomb at intersection var bombCandy = groupA[a]; if (bombCandy.special === SPECIAL_NONE) { bombCandy.special = SPECIAL_BOMB; bombCandy.setType(bombCandy.type, SPECIAL_BOMB, bombCandy.blocker); } } } } } } return unique; } // Remove matched candies and animate, including special candy activation and blockers function removeMatches(matches, cb) { if (!matches || matches.length === 0) { if (cb) cb(); return; } LK.getSound('match').play(); var done = 0; var toRemove = []; // Activate special candies in matches for (var i = 0; i < matches.length; ++i) { var candy = matches[i]; if (candy.special === SPECIAL_STRIPED) { // Striped candy: clear row if created from horizontal match, column if from vertical // Determine orientation by checking if the special was created from a horizontal or vertical match // We'll use a property set during match detection, or fallback to random if not set (legacy) var isHorizontal = false; if (typeof candy._stripedOrientation !== "undefined") { isHorizontal = candy._stripedOrientation === "horizontal"; } else { // Fallback: random (legacy, but should not happen with new match logic) isHorizontal = Math.random() < 0.5; } if (isHorizontal) { // Clear row for (var c = 0; c < BOARD_COLS; ++c) { var target = board[candy.row][c]; if (toRemove.indexOf(target) === -1) toRemove.push(target); } } else { // Clear column for (var r = 0; r < BOARD_ROWS; ++r) { var target = board[r][candy.col]; if (toRemove.indexOf(target) === -1) toRemove.push(target); } } } else if (candy.special === SPECIAL_RAINBOW) { // Clear all candies of a random type on board var colorType = getRandomCandyType(); for (var r = 0; r < BOARD_ROWS; ++r) { for (var c = 0; c < BOARD_COLS; ++c) { var target = board[r][c]; if (target.type === colorType && toRemove.indexOf(target) === -1) toRemove.push(target); } } } else if (candy.special === SPECIAL_BOMB) { // If this bomb was created by a 2x2 square match, do a shine and destroy 8 neighbors if (candy._pendingSquareBomb) { // Shine effect: flash the bomb candy LK.effects.flashObject(candy, 0xffff00, 400); // Destroy 8 neighbors (not self) for (var dr = -1; dr <= 1; ++dr) { for (var dc = -1; dc <= 1; ++dc) { var rr = candy.row + dr, cc = candy.col + dc; if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) { if (rr === candy.row && cc === candy.col) continue; // skip self var target = board[rr][cc]; if (toRemove.indexOf(target) === -1) toRemove.push(target); } } } // Remove the flag so it doesn't trigger again delete candy._pendingSquareBomb; } else { // Default bomb: Clear 3x3 area (including self) for (var dr = -1; dr <= 1; ++dr) { for (var dc = -1; dc <= 1; ++dc) { var rr = candy.row + dr, cc = candy.col + dc; if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) { var target = board[rr][cc]; if (toRemove.indexOf(target) === -1) toRemove.push(target); } } } } } else { if (toRemove.indexOf(candy) === -1) toRemove.push(candy); } } // Remove blockers if matched for (var i = 0; i < toRemove.length; ++i) { var candy = toRemove[i]; if (candy.blocker === BLOCKER_CHOCOLATE) { // Remove chocolate in one match candy.blocker = BLOCKER_NONE; candy.setType(candy.type, candy.special, BLOCKER_NONE); continue; } if (candy.blocker === BLOCKER_ICE) { // Remove ice in two matches: first match cracks, second removes if (!candy._iceCracked) { candy._iceCracked = true; // Tint or visually indicate cracked ice (optional) candy.setType(candy.type, candy.special, BLOCKER_ICE); // Do not mark as matched, so it remains for the next match continue; } else { candy.blocker = BLOCKER_NONE; delete candy._iceCracked; candy.setType(candy.type, candy.special, BLOCKER_NONE); // Now allow to be matched and removed continue; } } candy.isMatched = true; // Animate scale down and fade tween(candy, { scaleX: 0, scaleY: 0, alpha: 0 }, { duration: 200, easing: tween.easeIn, onFinish: function (candy) { return function () { if (candy.asset) { candy.removeChild(candy.asset); candy.asset.destroy(); candy.asset = null; } candy.visible = false; done++; if (done === toRemove.length && cb) cb(); }; }(candy) }); // Add score score += 100; } } // Drop candies to fill empty spaces function dropCandies(cb) { var moved = false; for (var col = 0; col < BOARD_COLS; ++col) { for (var row = BOARD_ROWS - 1; row >= 0; --row) { var candy = board[row][col]; if (!candy.isMatched && candy.visible) continue; // Find nearest above for (var above = row - 1; above >= 0; --above) { var aboveCandy = board[above][col]; if (!aboveCandy.isMatched && aboveCandy.visible) { // Only move if the target cell is not already occupied by a visible, non-matched candy if (!board[row][col].visible || board[row][col].isMatched) { // Only allow candies to fall if there is a candy above (do not close the top of the column) board[row][col] = aboveCandy; aboveCandy.row = row; aboveCandy.col = col; aboveCandy.moveTo(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 180); board[above][col] = candy; moved = true; } break; } } // Do not close the top of the column: if there is no candy above, leave the cell empty for refillBoard } } if (cb) LK.setTimeout(cb, moved ? 200 : 0); } // Fill empty spaces with new candies function refillBoard(cb) { var created = false; for (var col = 0; col < BOARD_COLS; ++col) { for (var row = 0; row < BOARD_ROWS; ++row) { var candy = board[row][col]; if (!candy.isMatched && candy.visible) continue; // Only create a new candy if the cell is not already occupied by a visible, non-matched candy if (!board[row][col].visible || board[row][col].isMatched) { // Create new candy var newCandy = new Candy(); newCandy.row = row; newCandy.col = col; newCandy.x = col * CELL_SIZE + CELL_SIZE / 2; newCandy.y = -CELL_SIZE + CELL_SIZE / 2; // Build a list of all asset names that are required for objectives in this level var objectiveAssetNames = []; for (var i = 0; i < currentObjectives.length; ++i) { if (currentObjectives[i].asset) { if (objectiveAssetNames.indexOf(currentObjectives[i].asset) === -1) { objectiveAssetNames.push(currentObjectives[i].asset); } } } // If there are any objective assets, randomly pick one to spawn with lower probability // Reduce probability to 25% to lower chance of self-exploding objective boxes if (objectiveAssetNames.length > 0 && Math.random() < 0.25) { // --- Set random proportions for each objective asset for this level (reuse from initBoard) --- if (!window._objectiveAssetProportions || window._objectiveAssetProportionsLevel !== currentLevel) { window._objectiveAssetProportions = {}; var total = 0; for (var i = 0; i < objectiveAssetNames.length; ++i) { // Give each asset a random weight between 1 and 100 var w = randInt(1, 100); window._objectiveAssetProportions[objectiveAssetNames[i]] = w; total += w; } // Normalize to probabilities (sum to 1) for (var i = 0; i < objectiveAssetNames.length; ++i) { var k = objectiveAssetNames[i]; window._objectiveAssetProportions[k] /= total; } window._objectiveAssetProportionsLevel = currentLevel; } // Weighted random pick var r = Math.random(); var acc = 0; var chosenAsset = objectiveAssetNames[0]; for (var i = 0; i < objectiveAssetNames.length; ++i) { var k = objectiveAssetNames[i]; acc += window._objectiveAssetProportions[k]; if (r <= acc) { chosenAsset = k; break; } } newCandy.setType(chosenAsset, SPECIAL_NONE, BLOCKER_NONE); } else { // Otherwise, use a random candy type newCandy.setType(getRandomCandyType(), SPECIAL_NONE, BLOCKER_NONE); } // Always add new candies to the top of the boardContainer to ensure correct z-ordering boardContainer.addChild(newCandy); if (boardContainer.children && boardContainer.children.length > 1) { // Move the newly added candy to the top (end of children array) boardContainer.removeChild(newCandy); boardContainer.addChild(newCandy); } candies.push(newCandy); board[row][col] = newCandy; newCandy.moveTo(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 220); created = true; } } } if (cb) LK.setTimeout(cb, created ? 220 : 0); } // Remove matched candies from board function clearMatchedCandies() { for (var row = 0; row < BOARD_ROWS; ++row) { for (var col = 0; col < BOARD_COLS; ++col) { var candy = board[row][col]; if (candy.isMatched) { if (candy._iceCracked) { delete candy._iceCracked; } candy.destroyCandy(); // Remove from candies array for (var i = 0; i < candies.length; ++i) { if (candies[i] === candy) { candies.splice(i, 1); break; } } // Replace with dummy invisible candy for drop logic var dummy = new Candy(); dummy.row = row; dummy.col = col; dummy.visible = false; board[row][col] = dummy; // Always add dummy candies to the bottom of the boardContainer so they don't cover visible candies boardContainer.addChildAt(dummy, 0); } } } } // Deselect all candies function deselectAll() { for (var i = 0; i < candies.length; ++i) { candies[i].setSelected(false); } selectedCandy = null; } // Handle user tap function handleTap(x, y, obj) { if (swapping || animating || isProcessing) return; // Convert to board coordinates, ensuring correct offset and boundaries // Remove any offset miscalculations: boardContainer.x/y is already set, so local is relative to grid origin // For touch/click, always use the event's x/y relative to the game, then subtract boardContainer.x/y to get local grid coordinates var localX = x - boardContainer.x; var localY = y - boardContainer.y; // Clamp localX/localY to be within the grid area if (localX < 0 || localY < 0) return; var col = Math.floor(localX / CELL_SIZE); var row = Math.floor(localY / CELL_SIZE); // Clamp to grid bounds // Boundary check if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return; var candy = board[row][col]; // Debug: Draw a highlight rectangle over the selected cell if (typeof handleTap._debugRect !== "undefined" && handleTap._debugRect) { boardContainer.removeChild(handleTap._debugRect); handleTap._debugRect.destroy(); handleTap._debugRect = null; } var debugRect = LK.getAsset('blocker_ice', { anchorX: 0, anchorY: 0, x: col * CELL_SIZE, y: row * CELL_SIZE, width: CELL_SIZE, height: CELL_SIZE, alpha: 0.25 }); // Insert debugRect at the bottom of boardContainer's children so it doesn't block candy input if (boardContainer.children && boardContainer.children.length > 0) { boardContainer.addChildAt(debugRect, 0); } else { boardContainer.addChild(debugRect); } handleTap._debugRect = debugRect; if (!candy || candy.blocker !== BLOCKER_NONE) return; // --- Bonus: Bomb usage --- if (selectedBonus === "bomb") { // Use bomb on this cell: destroy up to 12 tiles in a cross pattern (center, 4 orthogonal, 4 diagonal, and 3 more in a star/cross) var toRemove = []; var bombPattern = [[0, 0], // center [-1, 0], [1, 0], [0, -1], [0, 1], // orthogonal [-1, -1], [-1, 1], [1, -1], [1, 1], // diagonal [-2, 0], [2, 0], [0, -2], [0, 2] // extended cross ]; for (var i = 0; i < bombPattern.length; ++i) { var dr = bombPattern[i][0]; var dc = bombPattern[i][1]; var rr = row + dr; var cc = col + dc; if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) { var target = board[rr][cc]; if (target && target.blocker === BLOCKER_NONE && toRemove.indexOf(target) === -1) { toRemove.push(target); } } } removeMatches(toRemove, function () { clearMatchedCandies(); dropCandies(function () { refillBoard(function () { processMatches(); }); }); }); movesLeft--; // Using a bonus still costs a move if (movesLeft < 0) movesLeft = 0; if (movesLeft < 0) movesLeft = 0; updateGUI(); selectedBonus = null; bombBtnBg.scaleX = bombBtnBg.scaleY = 1.0; deselectAll(); return; } // If nothing selected, select this candy if (!selectedCandy) { deselectAll(); selectedCandy = candy; candy.setSelected(true); return; } // If clicking the same candy, deselect if (selectedCandy === candy) { deselectAll(); return; } // If adjacent, try to swap if (areAdjacent(selectedCandy, candy)) { swapping = true; selectedCandy.setSelected(false); candy.setSelected(false); // Lock input during animation var c1 = selectedCandy; var c2 = candy; deselectAll(); swapCandies(c1, c2, function () { // Special candy swap logic var specialActivated = false; // Rainbow (color bomb) swap if (c1.special === SPECIAL_RAINBOW || c2.special === SPECIAL_RAINBOW) { var colorType = c1.special === SPECIAL_RAINBOW ? c2.type : c1.type; var toRemove = []; for (var r = 0; r < BOARD_ROWS; ++r) { for (var c = 0; c < BOARD_COLS; ++c) { var target = board[r][c]; if (target.type === colorType) toRemove.push(target); } } removeMatches(toRemove, function () { clearMatchedCandies(); dropCandies(function () { refillBoard(function () { processMatches(); }); }); }); movesLeft--; if (movesLeft < 0) movesLeft = 0; updateGUI(); swapping = false; specialActivated = true; } // Striped + striped: clear row and col else if (c1.special === SPECIAL_STRIPED && c2.special === SPECIAL_STRIPED) { var toRemove = []; for (var c = 0; c < BOARD_COLS; ++c) { var t1 = board[c1.row][c]; if (toRemove.indexOf(t1) === -1) toRemove.push(t1); } for (var r = 0; r < BOARD_ROWS; ++r) { var t2 = board[r][c2.col]; if (toRemove.indexOf(t2) === -1) toRemove.push(t2); } removeMatches(toRemove, function () { clearMatchedCandies(); dropCandies(function () { refillBoard(function () { processMatches(); }); }); }); movesLeft--; if (movesLeft < 0) movesLeft = 0; updateGUI(); swapping = false; specialActivated = true; } // Bomb + any: clear 3x3 around both else if (c1.special === SPECIAL_BOMB || c2.special === SPECIAL_BOMB) { var toRemove = []; var bombCandies = [c1, c2]; for (var b = 0; b < 2; ++b) { if (bombCandies[b].special === SPECIAL_BOMB) { for (var dr = -1; dr <= 1; ++dr) { for (var dc = -1; dc <= 1; ++dc) { var rr = bombCandies[b].row + dr, cc = bombCandies[b].col + dc; if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) { var target = board[rr][cc]; if (toRemove.indexOf(target) === -1) toRemove.push(target); } } } } } removeMatches(toRemove, function () { clearMatchedCandies(); dropCandies(function () { refillBoard(function () { processMatches(); }); }); }); movesLeft--; if (movesLeft < 0) movesLeft = 0; updateGUI(); swapping = false; specialActivated = true; } if (!specialActivated) { // After swap, check for matches var matches = findMatches(); if (matches.length > 0) { processMatches(); movesLeft--; if (movesLeft < 0) movesLeft = 0; updateGUI(); swapping = false; } else { // No match, swap back LK.getSound('fail').play(); // Animate a quick shake for both candies var origX1 = c1.col * CELL_SIZE + CELL_SIZE / 2; var origY1 = c1.row * CELL_SIZE + CELL_SIZE / 2; var origX2 = c2.col * CELL_SIZE + CELL_SIZE / 2; var origY2 = c2.row * CELL_SIZE + CELL_SIZE / 2; tween(c1, { x: origX1 + 20 }, { duration: 60, onFinish: function onFinish() { tween(c1, { x: origX1 }, { duration: 60 }); } }); tween(c2, { x: origX2 - 20 }, { duration: 60, onFinish: function onFinish() { tween(c2, { x: origX2 }, { duration: 60 }); } }); // Actually swap back the candies to their original positions in the board and update their row/col swapCandies(c1, c2, function () { swapping = false; deselectAll(); // Re-select the original candy for user feedback selectedCandy = c1; c1.setSelected(true); }); movesLeft--; if (movesLeft < 0) movesLeft = 0; updateGUI(); } } }); return; } // Not adjacent, select new candy deselectAll(); selectedCandy = candy; candy.setSelected(true); } // Process matches and refill function processMatches() { isProcessing = true; var matches = findMatches(); if (matches.length === 0) { swapping = false; isProcessing = false; // Defensive: ensure isProcessing is false after resetBtn isProcessing = false; deselectAll(); // Save progress after move storage.currentLevel = currentLevel; storage.score = score; storage.movesLeft = movesLeft; checkGameEnd(); return; } removeMatches(matches, function () { clearMatchedCandies(); dropCandies(function () { refillBoard(function () { // --- Update objective progress after all matches/refills --- for (var i = 0; i < currentObjectives.length; ++i) { var obj = currentObjectives[i]; if (obj.type === OBJECTIVE_COLLECT) { // Count how many of this mechanical asset were matched (destroyed) this turn var matched = 0; for (var j = 0; j < matches.length; ++j) { if (matches[j].type === obj.asset && matches[j].isMatched) { matched++; } else if (matches[j].isMatched && matches[j].type && obj.asset && matches[j].type !== obj.asset) { // Error log if a match is counted for the wrong asset console.error("Objective mismatch: tried to count " + matches[j].type + " for " + obj.asset); } } currentObjectiveProgress[i] += matched; } else if (obj.type === OBJECTIVE_CLEAR) { // Count blockers destroyed (match asset to correct blocker asset) var cleared = 0; for (var j = 0; j < matches.length; ++j) { // Only count if the matched candy/blocker was actually destroyed and matches the correct asset if (matches[j].isMatched && obj.asset) { // Map blocker type to asset name var blockerAsset = ''; if (matches[j].blocker === BLOCKER_CHOCOLATE) blockerAsset = 'blocker_chocolate'; if (matches[j].blocker === BLOCKER_ICE) blockerAsset = 'blocker_ice'; // Only count if the asset matches the objective asset if (blockerAsset === obj.asset) { cleared++; } } } currentObjectiveProgress[i] += cleared; } else if (obj.type === OBJECTIVE_RESCUE) { // Not used in mechanical theme, but left for extensibility var rescued = 0; for (var j = 0; j < matches.length; ++j) { if (matches[j].special === SPECIAL_BOMB && matches[j].row === BOARD_ROWS - 1 && matches[j].isMatched) rescued++; } currentObjectiveProgress[i] += rescued; } // Clamp to max if (currentObjectiveProgress[i] > obj.count) currentObjectiveProgress[i] = obj.count; } updateProgressPanel(); updateStarPanel(); updateCounterPanel(); // --- Timed Bombs: decrement and check for explosion --- for (var t = timedBombs.length - 1; t >= 0; --t) { timedBombs[t].movesLeft--; if (timedBombs[t].movesLeft <= 0) { // Bomb explodes: game over LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); isProcessing = false; // Defensive: ensure isProcessing is false after timed bomb explosion isProcessing = false; LK.setTimeout(function () { // Reset current level: reset score, moves, board score = 0; // Always reset movesLeft to the initial moves for this level, never negative movesLeft = Math.max(0, levels[currentLevel].moves); targetScore = levels[currentLevel].target; storage.currentLevel = currentLevel; storage.score = score; storage.movesLeft = movesLeft; updateGUI(); initBoard(); updateCounterPanel(); isProcessing = false; }, 1200); // Wait for game over animation to finish before restarting return; } } // --- Conveyor: shift candies if active --- if (conveyorActive) { shiftConveyor(); } // Chocolate spread: after all moves, if any chocolate exists, spread to adjacent var chocolateList = []; for (var row = 0; row < BOARD_ROWS; ++row) { for (var col = 0; col < BOARD_COLS; ++col) { var c = board[row][col]; if (c.blocker === BLOCKER_CHOCOLATE) chocolateList.push(c); } } if (chocolateList.length > 0) { // Try to spread chocolate to adjacent non-blocker, non-special, non-chocolate for (var i = 0; i < chocolateList.length; ++i) { var c = chocolateList[i]; var dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]]; for (var d = 0; d < dirs.length; ++d) { var rr = c.row + dirs[d][0], cc = c.col + dirs[d][1]; if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) { var target = board[rr][cc]; if (target.blocker === BLOCKER_NONE && target.special === SPECIAL_NONE) { target.blocker = BLOCKER_CHOCOLATE; target.setType(target.type, target.special, BLOCKER_CHOCOLATE); break; } } } } } // After all refills, check for new matches var newMatches = findMatches(); if (newMatches.length > 0) { // Defensive: reset isProcessing before recursion to avoid freeze if processMatches is called recursively isProcessing = false; processMatches(); return; // Prevent further code execution after recursion } else { swapping = false; isProcessing = false; deselectAll(); // Save progress after move storage.currentLevel = currentLevel; storage.score = score; storage.movesLeft = movesLeft; // --- Check for win/lose conditions --- if (allObjectivesComplete()) { // Celebrate: flash screen, animate stars, etc. for (var s = 0; s < 3; ++s) { if (starIcons[s].alpha === 1) LK.effects.flashObject(starIcons[s], 0xffff00, 800); } LK.effects.flashScreen(0x00ff00, 1000); // Show next level screen after passing the chapter isProcessing = false; // Defensive: ensure isProcessing is false after win isProcessing = false; LK.setTimeout(function () { // Advance to next level if not at last level if (currentLevel < levels.length - 1) { currentLevel++; } else { // If at last level, loop to first or show a "congratulations" message currentLevel = 0; } // Reset score, moves, and target for new level score = 0; movesLeft = levels[currentLevel].moves; targetScore = levels[currentLevel].target; storage.currentLevel = currentLevel; storage.score = score; storage.movesLeft = movesLeft; updateGUI(); initBoard(); updateCounterPanel(); // Show a "Next Level" overlay if (typeof window.nextLevelScreen === "undefined") { window.nextLevelScreen = new Container(); LK.gui.center.addChild(window.nextLevelScreen); window.nextLevelScreen.visible = false; // Background var nextBg = LK.getAsset('blocker_ice', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, width: 1200, height: 1200, alpha: 0.85 }); window.nextLevelScreen.addChild(nextBg); // Title var nextTitle = new Text2('Next Level!', { size: 160, fill: "#fff" }); nextTitle.anchor.set(0.5, 0.5); nextTitle.x = 0; nextTitle.y = -200; window.nextLevelScreen.addChild(nextTitle); // Level number window.nextLevelScreen.levelNumTxt = new Text2('', { size: 100, fill: "#fff" }); window.nextLevelScreen.levelNumTxt.anchor.set(0.5, 0.5); window.nextLevelScreen.levelNumTxt.x = 0; window.nextLevelScreen.levelNumTxt.y = 0; window.nextLevelScreen.addChild(window.nextLevelScreen.levelNumTxt); // Play button var nextPlayBtn = new Container(); var nextPlayBtnBg = LK.getAsset('start_button', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, width: 500, height: 160 }); nextPlayBtn.addChild(nextPlayBtnBg); nextPlayBtn.anchorX = 0.5; nextPlayBtn.anchorY = 0.5; nextPlayBtn.x = 0; nextPlayBtn.y = 250; nextPlayBtn.interactive = true; nextPlayBtn.buttonMode = true; nextPlayBtn.down = function (x, y, obj) { window.nextLevelScreen.visible = false; // Show game UI if (scoreTxt) scoreTxt.visible = true; if (movesTxt) movesTxt.visible = true; if (targetTxt) targetTxt.visible = true; if (levelTopRightTxt) levelTopRightTxt.visible = true; if (bonusPanel) bonusPanel.visible = true; if (boardContainer) boardContainer.visible = true; if (levelTxt) levelTxt.visible = true; if (prevLevelBtn) prevLevelBtn.visible = true; if (resetBtn) resetBtn.visible = true; if (window.levelDescTxt) window.levelDescTxt.visible = true; if (counterPanel) counterPanel.visible = true; }; window.nextLevelScreen.addChild(nextPlayBtn); } // Update level number window.nextLevelScreen.levelNumTxt.setText('Level: ' + (currentLevel + 1)); window.nextLevelScreen.visible = true; // Hide game UI while next level screen is visible if (scoreTxt) scoreTxt.visible = false; if (movesTxt) movesTxt.visible = false; if (targetTxt) targetTxt.visible = false; if (levelTopRightTxt) levelTopRightTxt.visible = false; if (bonusPanel) bonusPanel.visible = false; if (boardContainer) boardContainer.visible = false; if (levelTxt) levelTxt.visible = false; if (prevLevelBtn) prevLevelBtn.visible = false; if (resetBtn) resetBtn.visible = false; if (window.levelDescTxt) window.levelDescTxt.visible = false; if (counterPanel) counterPanel.visible = false; }, 1200); // Wait for win animation to finish before showing next level screen return; } checkGameEnd(); } }); }); }); } // Update GUI function updateGUI() { scoreTxt.setText('Score: ' + score); movesTxt.setText('Moves: ' + movesLeft); targetTxt.setText('Target: ' + targetScore); // Show "Boss" for every 5th level (level 5, 10, 15, ...) var levelNum = currentLevel + 1; if (levelNum % 5 === 0) { levelTxt.setText('Level: ' + levelNum + ' (Boss!)'); if (typeof levelTopRightTxt !== "undefined") { levelTopRightTxt.setText('Level: ' + levelNum + ' (Boss!)'); } } else { levelTxt.setText('Level: ' + levelNum); if (typeof levelTopRightTxt !== "undefined") { levelTopRightTxt.setText('Level: ' + levelNum); } } // Show level description and pass requirement at the bottom center if (!window.levelDescTxt) { window.levelDescTxt = new Text2('', { size: 60, fill: "#fff" }); window.levelDescTxt.anchor.set(0.5, 1); LK.gui.bottom.addChild(window.levelDescTxt); window.levelDescTxt.y = -100; window.levelDescTxt.visible = false; } // Format description to be 3 lines below each other var desc = levels[currentLevel].description || ''; var descLines = desc.split('\n'); while (descLines.length < 3) descLines.push(''); // Show objectives as requirements for (var i = 0; i < currentObjectives.length; ++i) { var obj = currentObjectives[i]; if (obj.type === OBJECTIVE_COLLECT) { var assetName = obj.asset ? obj.asset.replace('candy_', '').replace('blocker_', '') : ''; descLines.push('Collect ' + obj.count + ' ' + assetName); } if (obj.type === OBJECTIVE_CLEAR) { var assetName = obj.asset ? obj.asset.replace('candy_', '').replace('blocker_', '') : ''; descLines.push('Clear ' + obj.count + ' ' + assetName); } if (obj.type === OBJECTIVE_RESCUE) descLines.push('Rescue ' + obj.count + ' mechanics'); } window.levelDescTxt.setText(descLines.join('\n')); updateProgressPanel(); updateStarPanel(); updateCounterPanel(); } // Check for win/lose function checkGameEnd() { // Game over if out of moves and not all objectives complete if (movesLeft <= 0) { if (allObjectivesComplete()) { LK.showYouWin(); } else { LK.showGameOver(); isProcessing = false; // Defensive: ensure isProcessing is false after game over isProcessing = false; LK.setTimeout(function () { // Reset current level: reset score, moves, board score = 0; // Always reset movesLeft to the initial moves for this level, never negative movesLeft = Math.max(0, levels[currentLevel].moves); targetScore = levels[currentLevel].target; storage.currentLevel = currentLevel; storage.score = score; storage.movesLeft = movesLeft; updateGUI(); initBoard(); updateCounterPanel(); isProcessing = false; }, 1200); // Wait for game over animation to finish before restarting } } } // Game event handlers game.down = function (x, y, obj) { // Don't allow tap in top left 100x100 if (x < 100 && y < 100) return; handleTap(x, y, obj); }; // Drag-to-swap logic var dragStartCandy = null; var dragCurrentCandy = null; var dragActive = false; // Helper: get candy at pixel position (returns null if not valid) function getCandyAtPixel(x, y) { var localX = x - boardContainer.x; var localY = y - boardContainer.y; if (localX < 0 || localY < 0) return null; var col = Math.floor(localX / CELL_SIZE); var row = Math.floor(localY / CELL_SIZE); if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return null; return board[row][col]; } // Override game.down for drag start game.down = function (x, y, obj) { // Don't allow tap in top left 100x100 if (x < 100 && y < 100) return; // If bomb bonus is selected, allow placing bomb anywhere on the board if (selectedBonus === "bomb") { // Convert to board coordinates var localX = x - boardContainer.x; var localY = y - boardContainer.y; if (localX < 0 || localY < 0) return; var col = Math.floor(localX / CELL_SIZE); var row = Math.floor(localY / CELL_SIZE); if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return; var candy = board[row][col]; // Only allow bomb on non-blocker if (!candy || candy.blocker !== BLOCKER_NONE) return; // Use bomb on this cell: destroy up to 12 tiles in a cross pattern (center, 4 orthogonal, 4 diagonal, and 3 more in a star/cross) var toRemove = []; var bombPattern = [[0, 0], [-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1], [-2, 0], [2, 0], [0, -2], [0, 2]]; for (var i = 0; i < bombPattern.length; ++i) { var dr = bombPattern[i][0]; var dc = bombPattern[i][1]; var rr = row + dr; var cc = col + dc; if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) { var target = board[rr][cc]; if (target && target.blocker === BLOCKER_NONE && toRemove.indexOf(target) === -1) { toRemove.push(target); } } } removeMatches(toRemove, function () { clearMatchedCandies(); dropCandies(function () { refillBoard(function () { processMatches(); }); }); }); movesLeft--; // Using a bonus still costs a move if (movesLeft < 0) movesLeft = 0; updateGUI(); selectedBonus = null; bombBtnBg.scaleX = bombBtnBg.scaleY = 1.0; deselectAll(); return; } if (swapping || animating || isProcessing) return; var candy = getCandyAtPixel(x, y); if (!candy || candy.blocker !== BLOCKER_NONE) return; dragStartCandy = candy; dragCurrentCandy = candy; dragActive = true; deselectAll(); candy.setSelected(true); selectedCandy = candy; }; // Drag move handler game.move = function (x, y, obj) { if (!dragActive || swapping || animating || isProcessing) return; var candy = getCandyAtPixel(x, y); if (!candy || candy.blocker !== BLOCKER_NONE) return; if (candy === dragStartCandy) return; // Only allow swap with adjacent if (areAdjacent(dragStartCandy, candy)) { // Lock drag dragActive = false; dragStartCandy.setSelected(false); candy.setSelected(false); var c1 = dragStartCandy; var c2 = candy; deselectAll(); swapCandies(c1, c2, function () { // Special candy swap logic (same as tap) var specialActivated = false; if (c1.special === SPECIAL_RAINBOW || c2.special === SPECIAL_RAINBOW) { var colorType = c1.special === SPECIAL_RAINBOW ? c2.type : c1.type; var toRemove = []; for (var r = 0; r < BOARD_ROWS; ++r) { for (var c = 0; c < BOARD_COLS; ++c) { var target = board[r][c]; if (target.type === colorType) toRemove.push(target); } } removeMatches(toRemove, function () { clearMatchedCandies(); dropCandies(function () { refillBoard(function () { processMatches(); }); }); }); movesLeft--; if (movesLeft < 0) movesLeft = 0; updateGUI(); swapping = false; specialActivated = true; } else if (c1.special === SPECIAL_STRIPED && c2.special === SPECIAL_STRIPED) { var toRemove = []; for (var c = 0; c < BOARD_COLS; ++c) { var t1 = board[c1.row][c]; if (toRemove.indexOf(t1) === -1) toRemove.push(t1); } for (var r = 0; r < BOARD_ROWS; ++r) { var t2 = board[r][c2.col]; if (toRemove.indexOf(t2) === -1) toRemove.push(t2); } removeMatches(toRemove, function () { clearMatchedCandies(); dropCandies(function () { refillBoard(function () { processMatches(); }); }); }); movesLeft--; if (movesLeft < 0) movesLeft = 0; updateGUI(); swapping = false; specialActivated = true; } else if (c1.special === SPECIAL_BOMB || c2.special === SPECIAL_BOMB) { var toRemove = []; var bombCandies = [c1, c2]; for (var b = 0; b < 2; ++b) { if (bombCandies[b].special === SPECIAL_BOMB) { for (var dr = -1; dr <= 1; ++dr) { for (var dc = -1; dc <= 1; ++dc) { var rr = bombCandies[b].row + dr, cc = bombCandies[b].col + dc; if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) { var target = board[rr][cc]; if (toRemove.indexOf(target) === -1) toRemove.push(target); } } } } } removeMatches(toRemove, function () { clearMatchedCandies(); dropCandies(function () { refillBoard(function () { processMatches(); }); }); }); movesLeft--; if (movesLeft < 0) movesLeft = 0; updateGUI(); swapping = false; specialActivated = true; } if (!specialActivated) { var matches = findMatches(); if (matches.length > 0) { processMatches(); movesLeft--; if (movesLeft < 0) movesLeft = 0; updateGUI(); swapping = false; } else { // No match, swap back LK.getSound('fail').play(); var origX1 = c1.col * CELL_SIZE + CELL_SIZE / 2; var origY1 = c1.row * CELL_SIZE + CELL_SIZE / 2; var origX2 = c2.col * CELL_SIZE + CELL_SIZE / 2; var origY2 = c2.row * CELL_SIZE + CELL_SIZE / 2; tween(c1, { x: origX1 + 20 }, { duration: 60, onFinish: function onFinish() { tween(c1, { x: origX1 }, { duration: 60 }); } }); tween(c2, { x: origX2 - 20 }, { duration: 60, onFinish: function onFinish() { tween(c2, { x: origX2 }, { duration: 60 }); } }); // Actually swap back the candies to their original positions in the board and update their row/col swapCandies(c1, c2, function () { swapping = false; deselectAll(); // Re-select the original candy for user feedback selectedCandy = c1; c1.setSelected(true); }); movesLeft--; if (movesLeft < 0) movesLeft = 0; updateGUI(); } } }); } }; // Drag end handler game.up = function (x, y, obj) { dragActive = false; dragStartCandy = null; dragCurrentCandy = null; // Deselect all if not swapping if (!swapping && !animating && !isProcessing) { deselectAll(); } }; // Game update game.update = function () { // No per-frame logic needed for MVP }; // Start music LK.playMusic('bgmusic', { fade: { start: 0, end: 0.7, duration: 1000 } }); // Level label at bottom left var levelTxt = new Text2('Level: 1', { size: 70, fill: "#fff" }); levelTxt.anchor.set(0, 1); LK.gui.bottomLeft.addChild(levelTxt); levelTxt.x = 0; levelTxt.y = 0; levelTxt.visible = false; // Add previous level button at the bottom left (but not in the top left 100x100 area) var prevLevelBtn = new Text2('Prev', { size: 90, fill: "#fff" }); prevLevelBtn.anchor.set(0, 1); LK.gui.bottomLeft.addChild(prevLevelBtn); prevLevelBtn.x = 120; // avoid top left menu area prevLevelBtn.y = 0; prevLevelBtn.visible = false; prevLevelBtn.interactive = true; prevLevelBtn.buttonMode = true; prevLevelBtn.down = function (x, y, obj) { if (currentLevel > 0) { currentLevel--; score = 0; movesLeft = levels[currentLevel].moves; targetScore = levels[currentLevel].target; // Save progress storage.currentLevel = currentLevel; storage.score = score; storage.movesLeft = movesLeft; updateGUI(); initBoard(); updateCounterPanel(); isProcessing = false; // Defensive: ensure isProcessing is false after prevLevelBtn isProcessing = false; } }; // Add reset button at the bottom center var resetBtn = new Text2('Reset', { size: 90, fill: "#fff" }); resetBtn.anchor.set(0.5, 1); LK.gui.bottom.addChild(resetBtn); resetBtn.y = 0; // flush to bottom resetBtn.visible = false; resetBtn.interactive = true; resetBtn.buttonMode = true; resetBtn.down = function (x, y, obj) { // Reset current level: reset score, moves, board score = 0; movesLeft = levels[currentLevel].moves; targetScore = levels[currentLevel].target; // Save progress storage.currentLevel = currentLevel; storage.score = score; storage.movesLeft = movesLeft; updateGUI(); initBoard(); updateCounterPanel(); isProcessing = false; }; // --- Counter Panel Below Board --- // This panel will be centered horizontally, just below the board, and always visible var counterPanel = new Container(); game.addChild(counterPanel); // Position: center horizontally, just below the board counterPanel.x = BOARD_OFFSET_X + BOARD_COLS * CELL_SIZE / 2; counterPanel.y = BOARD_OFFSET_Y + BOARD_ROWS * CELL_SIZE + 40; // 40px below the board counterPanel.visible = true; // Counter text (will be updated in updateCounterPanel) var counterTxt = new Text2('', { size: 80, fill: "#fff" }); counterTxt.anchor.set(0.5, 0); counterPanel.addChild(counterTxt); // Helper: update counter panel with current progress function updateCounterPanel() { // Compose a summary of all objectives, each with its asset icon and progress // Remove old icons (keep counterTxt as first child) while (counterPanel.children.length > 1) { var ch = counterPanel.children.pop(); ch.destroy && ch.destroy(); } // For each objective, show icon and progress var summaryArr = []; var iconX = -((currentObjectives.length - 1) * 120) / 2; // space icons evenly for (var i = 0; i < currentObjectives.length; ++i) { var obj = currentObjectives[i]; var iconId = null; if (OBJECTIVE_ICONS[obj.mech]) { iconId = OBJECTIVE_ICONS[obj.mech]; } else if (OBJECTIVE_ICONS.fallback[obj.mech]) { iconId = OBJECTIVE_ICONS.fallback[obj.mech]; } else { iconId = OBJECTIVE_ICONS.fallback.valve; } var icon = LK.getAsset(iconId, { anchorX: 0.5, anchorY: 0.5, x: iconX + i * 120, y: 60, width: 80, height: 80 }); counterPanel.addChild(icon); // Progress text below icon var txt = new Text2(currentObjectiveProgress[i] + " / " + obj.count, { size: 48, fill: "#fff" }); txt.anchor.set(0.5, 0); txt.x = icon.x; txt.y = icon.y + 60; counterPanel.addChild(txt); } // Optionally, show a summary at the top (e.g. "Objectives") counterTxt.setText("Objectives"); } // Call updateCounterPanel whenever board is initialized or progress changes // Start game initBoard(); updateGUI(); updateCounterPanel(); // If the start screen is hidden (game started), show UI if (typeof startScreen !== "undefined" && !startScreen.visible) { if (scoreTxt) scoreTxt.visible = true; if (movesTxt) movesTxt.visible = true; if (targetTxt) targetTxt.visible = true; if (levelTopRightTxt) levelTopRightTxt.visible = true; if (bonusPanel) bonusPanel.visible = true; if (boardContainer) boardContainer.visible = true; if (levelTxt) levelTxt.visible = true; if (prevLevelBtn) prevLevelBtn.visible = true; if (resetBtn) resetBtn.visible = true; // Removed reference to nextLevelBtn which is not defined if (window.levelDescTxt) window.levelDescTxt.visible = true; if (counterPanel) counterPanel.visible = true; } else { // On opening, always hide UI until play is pressed if (scoreTxt) scoreTxt.visible = false; if (movesTxt) movesTxt.visible = false; if (targetTxt) targetTxt.visible = false; if (levelTopRightTxt) levelTopRightTxt.visible = false; if (bonusPanel) bonusPanel.visible = false; if (boardContainer) boardContainer.visible = false; if (levelTxt) levelTxt.visible = false; if (prevLevelBtn) prevLevelBtn.visible = false; if (resetBtn) resetBtn.visible = false; if (window.levelDescTxt) window.levelDescTxt.visible = false; if (counterPanel) counterPanel.visible = false; }
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Candy class
var Candy = Container.expand(function () {
var self = Container.call(this);
// Properties
self.type = getRandomCandyType();
self.special = SPECIAL_NONE;
self.blocker = BLOCKER_NONE;
self.row = 0;
self.col = 0;
self.isFalling = false;
self.isMatched = false;
self.isSelected = false;
self.asset = null;
// Attach asset
function updateAsset() {
if (self.asset) {
self.removeChild(self.asset);
self.asset.destroy();
}
var assetId = self.type;
if (self.special === SPECIAL_STRIPED) assetId = 'candy_striped';
if (self.special === SPECIAL_BOMB) assetId = 'candy_bomb';
if (self.special === SPECIAL_RAINBOW) assetId = 'candy_rainbow';
if (self.blocker === BLOCKER_CHOCOLATE) assetId = 'blocker_chocolate';
if (self.blocker === BLOCKER_ICE) assetId = 'blocker_ice';
self.asset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
if (self.isSelected) {
self.asset.scaleX = 1.15;
self.asset.scaleY = 1.15;
} else {
self.asset.scaleX = 1;
self.asset.scaleY = 1;
}
}
// Set type
self.setType = function (type, special, blocker) {
self.type = type || getRandomCandyType();
self.special = special || SPECIAL_NONE;
self.blocker = blocker || BLOCKER_NONE;
updateAsset();
};
// Set selected
self.setSelected = function (selected) {
self.isSelected = selected;
updateAsset();
};
// Animate to position
self.moveTo = function (x, y, duration, onFinish) {
tween(self, {
x: x,
y: y
}, {
duration: duration || 200,
easing: tween.easeInOut,
onFinish: onFinish
});
};
// Destroy
self.destroyCandy = function () {
if (self.asset) {
self.removeChild(self.asset);
self.asset.destroy();
self.asset = null;
}
self.destroy();
};
// Init
updateAsset();
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222244
});
/****
* Game Code
****/
// Fallback shapes for missing tracker icons (V/P/G/E/L/T)
// Tracker icons (64x64) for each mechanical part
// purple T-junction
// gray locked pipe
// green elbow
// red gauge
// orange pump
// blue valve
//{2.1}
// Music and sounds
// UI assets
// Blocker assets
// Candy assets
// Prism button asset sized for large UI button (500x160 in use, so use 500x160 for best quality)
// Candy and blocker assets sized to fit CELL_SIZE (200x200) for main board
// Candy types
// Music
// Sounds
// Blockers
// Special candies
// Candy shapes/colors
// Board data
var CANDY_TYPES = ['candy_red', 'candy_green', 'candy_blue', 'candy_yellow', 'candy_purple', 'candy_orange'];
// Special types
var SPECIAL_NONE = 0;
var SPECIAL_STRIPED = 1;
var SPECIAL_BOMB = 2;
var SPECIAL_RAINBOW = 3;
// Blocker types
var BLOCKER_NONE = 0;
var BLOCKER_CHOCOLATE = 1;
var BLOCKER_ICE = 2;
// Board size
var BOARD_COLS = 7;
var BOARD_ROWS = 9;
var CELL_SIZE = 200;
var BOARD_OFFSET_X = Math.floor((2048 - BOARD_COLS * CELL_SIZE) / 2);
var BOARD_OFFSET_Y = 300;
// Helper: get random candy type
function getRandomCandyType() {
return CANDY_TYPES[Math.floor(Math.random() * CANDY_TYPES.length)];
}
// Helper: get random int
function randInt(a, b) {
return a + Math.floor(Math.random() * (b - a + 1));
}
var board = [];
var candies = [];
var selectedCandy = null;
var swapping = false;
var animating = false;
// --- Level Objective System ---
// Objective types: 1) Collect Targets, 2) Clear Obstacles, 3) Rescue Items
var OBJECTIVE_COLLECT = 1;
var OBJECTIVE_CLEAR = 2;
var OBJECTIVE_RESCUE = 3;
// Mechanical asset mapping for objectives and icons
var OBJECTIVE_ICONS = {
valve: 'icon_valve_blue',
pump: 'icon_pump_orange',
gauge: 'icon_gauge_red',
elbow: 'icon_elbow_green',
locked: 'icon_locked_pipe',
tjunction: 'icon_tjunction_purple',
fallback: {
valve: 'fallback_icon_V',
pump: 'fallback_icon_P',
gauge: 'fallback_icon_G',
elbow: 'fallback_icon_E',
locked: 'fallback_icon_L',
tjunction: 'fallback_icon_T'
}
};
// Per-level objectives (mechanical theme, all asset-linked, but reduce new asset types and connect new assets to missions in different quantities)
var levelObjectives = [
// Level 1: Collect 10 red candies (reuse candy_red asset)
[{
type: OBJECTIVE_COLLECT,
mech: 'valve',
asset: 'candy_red',
count: 10
}],
// Level 2: Collect 10 green candies (reuse candy_green asset)
[{
type: OBJECTIVE_COLLECT,
mech: 'pump',
asset: 'candy_green',
count: 10
}],
// Level 3: Collect 8 blue candies and 8 yellow candies (reuse candy_blue, candy_yellow)
[{
type: OBJECTIVE_COLLECT,
mech: 'gauge',
asset: 'candy_blue',
count: 8
}, {
type: OBJECTIVE_COLLECT,
mech: 'elbow',
asset: 'candy_yellow',
count: 8
}],
// Level 4: Clear 6 chocolate blockers, collect 6 purple candies
[{
type: OBJECTIVE_CLEAR,
mech: 'locked',
asset: 'blocker_chocolate',
count: 6
}, {
type: OBJECTIVE_COLLECT,
mech: 'tjunction',
asset: 'candy_purple',
count: 6
}],
// Level 5: Collect 7 orange candies, 7 blue candies
[{
type: OBJECTIVE_COLLECT,
mech: 'valve',
asset: 'candy_orange',
count: 7
}, {
type: OBJECTIVE_COLLECT,
mech: 'gauge',
asset: 'candy_blue',
count: 7
}],
// Level 6: Clear 8 ice blockers, collect 5 green candies
[{
type: OBJECTIVE_CLEAR,
mech: 'locked',
asset: 'blocker_ice',
count: 8
}, {
type: OBJECTIVE_COLLECT,
mech: 'elbow',
asset: 'candy_green',
count: 5
}],
// Level 7: Collect 5 striped candies, clear 5 chocolate blockers
[{
type: OBJECTIVE_COLLECT,
mech: 'tjunction',
asset: 'candy_striped',
count: 5
}, {
type: OBJECTIVE_CLEAR,
mech: 'locked',
asset: 'blocker_chocolate',
count: 5
}],
// Level 8: Collect 4 bombs, 4 rainbow candies (specials)
[{
type: OBJECTIVE_COLLECT,
mech: 'valve',
asset: 'candy_bomb',
count: 4
}, {
type: OBJECTIVE_COLLECT,
mech: 'gauge',
asset: 'candy_rainbow',
count: 4
}],
// Level 9: Clear 10 ice blockers, collect 3 of each color (red, green, blue, yellow, purple, orange)
[{
type: OBJECTIVE_CLEAR,
mech: 'locked',
asset: 'blocker_ice',
count: 10
}, {
type: OBJECTIVE_COLLECT,
mech: 'valve',
asset: 'candy_red',
count: 3
}, {
type: OBJECTIVE_COLLECT,
mech: 'pump',
asset: 'candy_green',
count: 3
}, {
type: OBJECTIVE_COLLECT,
mech: 'gauge',
asset: 'candy_blue',
count: 3
}, {
type: OBJECTIVE_COLLECT,
mech: 'elbow',
asset: 'candy_yellow',
count: 3
}, {
type: OBJECTIVE_COLLECT,
mech: 'tjunction',
asset: 'candy_purple',
count: 3
}, {
type: OBJECTIVE_COLLECT,
mech: 'valve',
asset: 'candy_orange',
count: 3
}]
// ...repeat or randomize for higher levels, always reusing existing assets and connecting them to missions in different quantities
];
// For levels > 10, cycle or randomize objectives for demo
function getObjectivesForLevel(level) {
if (level < levelObjectives.length) return levelObjectives[level];
// For higher levels, mix and increase counts
var base = levelObjectives[level % levelObjectives.length];
var newObj = [];
for (var i = 0; i < base.length; ++i) {
var o = {};
for (var k in base[i]) o[k] = base[i][k];
o.count = Math.ceil(o.count * (1 + level / 20));
newObj.push(o);
}
return newObj;
}
// Track progress for current objectives
var currentObjectives = [];
var currentObjectiveProgress = [];
// Progress panel UI
var progressPanel = new Container();
LK.gui.top.addChild(progressPanel);
progressPanel.x = 0;
progressPanel.y = 350;
progressPanel.visible = false;
// Process tracker panel (shows number of tasks performed and left, links to assets)
var processTrackerPanel = new Container();
LK.gui.top.addChild(processTrackerPanel);
processTrackerPanel.x = 0;
processTrackerPanel.y = 520;
processTrackerPanel.visible = false;
// Star bonus UI
var starPanel = new Container();
LK.gui.top.addChild(starPanel);
starPanel.x = 0;
starPanel.y = 420;
starPanel.visible = false;
var starIcons = [];
for (var i = 0; i < 3; ++i) {
var star = LK.getAsset('candy_rainbow', {
anchorX: 0.5,
anchorY: 0.5,
x: 80 * i,
y: 0,
width: 60,
height: 60,
alpha: 0.5
});
starPanel.addChild(star);
starIcons.push(star);
}
// Helper: update progress panel
function updateProgressPanel() {
// Remove old children
while (progressPanel.children && progressPanel.children.length > 0) {
var ch = progressPanel.children.pop();
ch.destroy && ch.destroy();
}
// For each objective, show icon, progress, and animation if completed
for (var i = 0; i < currentObjectives.length; ++i) {
var obj = currentObjectives[i];
var iconId = null;
// Use asset icon for collect/clear, fallback to mechanical icon
if ((obj.type === OBJECTIVE_COLLECT || obj.type === OBJECTIVE_CLEAR) && obj.asset) {
iconId = obj.asset;
} else if (OBJECTIVE_ICONS[obj.mech]) {
iconId = OBJECTIVE_ICONS[obj.mech];
} else if (OBJECTIVE_ICONS.fallback[obj.mech]) {
iconId = OBJECTIVE_ICONS.fallback[obj.mech];
} else {
iconId = OBJECTIVE_ICONS.fallback.valve; // fallback to V
}
var icon = LK.getAsset(iconId, {
anchorX: 0.5,
anchorY: 0.5,
x: 80 + i * 320,
y: 0,
width: 90,
height: 90
});
progressPanel.addChild(icon);
var txt = new Text2('', {
size: 60,
fill: "#fff"
});
txt.anchor.set(0, 0.5);
txt.x = 140 + i * 320;
txt.y = 0;
var val = currentObjectiveProgress[i];
var goal = obj.count;
txt.setText(val + " / " + goal);
progressPanel.addChild(txt);
// If completed, flash icon
if (val >= goal) {
LK.effects.flashObject(icon, 0x00ff00, 600);
}
}
}
// --- Process Tracker Panel: show number of tasks performed and left, link to assets ---
while (processTrackerPanel.children && processTrackerPanel.children.length > 0) {
var ch = processTrackerPanel.children.pop();
ch.destroy && ch.destroy();
}
// Count total and completed tasks
var totalTasks = currentObjectives.length;
var completedTasks = 0;
for (var i = 0; i < currentObjectives.length; ++i) {
if (currentObjectiveProgress[i] >= currentObjectives[i].count) completedTasks++;
}
// Show summary text
var summaryTxt = new Text2("Tasks: " + completedTasks + " / " + totalTasks, {
size: 54,
fill: "#fff"
});
summaryTxt.anchor.set(0, 0.5);
summaryTxt.x = 0;
summaryTxt.y = 0;
processTrackerPanel.addChild(summaryTxt);
// For each task, show icon and progress
for (var i = 0; i < currentObjectives.length; ++i) {
var obj = currentObjectives[i];
var iconId = null;
if ((obj.type === OBJECTIVE_COLLECT || obj.type === OBJECTIVE_CLEAR) && obj.asset) {
iconId = obj.asset;
} else if (obj.type === OBJECTIVE_RESCUE && obj.asset) {
iconId = obj.asset;
} else if (OBJECTIVE_ICONS[obj.mech]) {
iconId = OBJECTIVE_ICONS[obj.mech];
} else if (OBJECTIVE_ICONS.fallback[obj.mech]) {
iconId = OBJECTIVE_ICONS.fallback[obj.mech];
} else {
iconId = OBJECTIVE_ICONS.fallback.valve;
}
var icon = LK.getAsset(iconId, {
anchorX: 0.5,
anchorY: 0.5,
x: 220 + i * 320,
y: 0,
width: 70,
height: 70
});
processTrackerPanel.addChild(icon);
var txt = new Text2(currentObjectiveProgress[i] + " / " + obj.count, {
size: 44,
fill: "#fff"
});
txt.anchor.set(0, 0.5);
txt.x = 260 + i * 320;
txt.y = 0;
processTrackerPanel.addChild(txt);
// If completed, flash icon
if (currentObjectiveProgress[i] >= obj.count) {
LK.effects.flashObject(icon, 0x00ff00, 600);
}
}
// Show/hide process tracker panel based on game state
processTrackerPanel.visible = progressPanel.visible;
// Helper: update star panel
function updateStarPanel() {
var percent = movesLeft / levels[currentLevel].moves;
for (var i = 0; i < 3; ++i) {
starIcons[i].alpha = 0.3;
}
if (percent > 0.5) {
starIcons[0].alpha = 1;
starIcons[1].alpha = 1;
starIcons[2].alpha = 1;
} else if (percent > 0.25) {
starIcons[0].alpha = 1;
starIcons[1].alpha = 1;
} else if (percent > 0) {
starIcons[0].alpha = 1;
}
}
// Helper: check if all objectives are complete
function allObjectivesComplete() {
for (var i = 0; i < currentObjectives.length; ++i) {
if (currentObjectiveProgress[i] < currentObjectives[i].count) return false;
}
return true;
}
// --- Timed Bombs and Conveyor Belts (mechanics) ---
var timedBombs = []; // {candy, movesLeft}
var conveyorActive = false;
// Add a bomb to a random candy (for demo, add on level 4+)
function addTimedBomb() {
var candidates = [];
for (var i = 0; i < candies.length; ++i) {
var c = candies[i];
if (c.blocker === BLOCKER_NONE && c.special === SPECIAL_NONE) candidates.push(c);
}
if (candidates.length > 0) {
var c = candidates[randInt(0, candidates.length - 1)];
c.special = SPECIAL_BOMB;
c.setType(c.type, SPECIAL_BOMB, c.blocker);
timedBombs.push({
candy: c,
movesLeft: 3
});
}
}
// Conveyor: shift all candies right by 1 (for demo, level 6+)
function shiftConveyor() {
for (var row = 0; row < BOARD_ROWS; ++row) {
var last = board[row][BOARD_COLS - 1];
for (var col = BOARD_COLS - 1; col > 0; --col) {
board[row][col] = board[row][col - 1];
board[row][col].col = col;
board[row][col].moveTo(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 180);
}
board[row][0] = last;
last.col = 0;
last.moveTo(0 * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 180);
}
}
// --- Per-level winCondition helpers ---
// Returns true if all blockers (chocolate/ice) are cleared from the board
function allBlockersCleared() {
for (var row = 0; row < BOARD_ROWS; ++row) {
for (var col = 0; col < BOARD_COLS; ++col) {
var c = board[row][col];
// Only count blockers that are visible and not matched
if (c && c.visible !== false && c.isMatched !== true && (c.blocker === BLOCKER_CHOCOLATE || c.blocker === BLOCKER_ICE)) {
// For ice, also check if it's not just cracked (must be fully removed)
if (c.blocker === BLOCKER_ICE && !c._iceCracked) {
return false;
}
if (c.blocker === BLOCKER_CHOCOLATE) {
return false;
}
}
}
}
return true;
}
// Returns true if all candies are cleared (for e.g. 'explode all boxes' levels)
function allBoxesCleared() {
for (var row = 0; row < BOARD_ROWS; ++row) {
for (var col = 0; col < BOARD_COLS; ++col) {
var c = board[row][col];
// If any visible, non-matched, non-blocker candy remains, not cleared
if (c && c.visible !== false && c.isMatched !== true && c.blocker === BLOCKER_NONE) {
return false;
}
}
}
return true;
}
// Example: fun feature - birds that must reach the bottom and be freed
// Returns true if all birds are at the bottom row and visible
function allBirdsFreed() {
for (var i = 0; i < candies.length; ++i) {
var c = candies[i];
if (c.isBird && c.row !== BOARD_ROWS - 1) {
return false;
}
}
return true;
}
// --- Level definitions: 100 levels, each with unique win condition and fixed moves ---
// Level 1 is the easiest, level 100 is the hardest
var levels = [];
// Moves curve: tuned for difficulty and level progression
// Level 1-2: very easy, more moves; 3-5: easy; 6-10: normal; 11-20: moderate; 21-30: challenging; 31-50: hard; 51-100: expert
var movesCurve = [25, 22,
// 1-2: very easy
18, 17, 16,
// 3-5: easy
15, 15, 14, 14, 13,
// 6-10: normal
13, 13, 12, 12, 12, 11, 11, 11, 11, 11,
// 11-20: moderate
10, 10, 10, 10, 10, 9, 9, 9, 9, 9,
// 21-30: challenging
8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
// 31-40: hard
7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
// 41-50: hard
6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
// 51-60: expert
6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
// 61-70: expert
5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
// 71-80: expert
5, 5, 5, 5, 5, 5, 5, 5, 5, 5 // 81-90: expert
];
// Fill up to 100 levels with 5 moves for 91-100
while (movesCurve.length < 100) movesCurve.push(5);
for (var i = 0; i < 100; ++i) {
var level = {};
// Difficulty curve: more blockers, less moves, higher targets as level increases
if (i === 0) {
// Level 1: Easiest, no blockers
level.moves = movesCurve[i];
level.target = 400;
level.blockers = [];
level.description = "Explode all boxes!\nClear the board to win.\nNo blockers.";
} else if (i === 1) {
// Level 2: Chocolate row
level.moves = movesCurve[i];
level.target = 600;
level.blockers = [{
row: 4,
type: BLOCKER_CHOCOLATE
}];
level.description = "Clear all chocolate!\nRemove every blocker to win.";
} else if (i === 2) {
// Level 3: Ice at bottom
level.moves = movesCurve[i];
level.target = 800;
level.blockers = [{
row: 7,
type: BLOCKER_ICE
}, {
row: 8,
type: BLOCKER_ICE
}];
level.description = "Break all the ice!\nClear every blocker to win.";
} else if (i === 3) {
// Level 4: Zig-zag blockers
level.moves = movesCurve[i];
level.target = 1000;
level.blockers = [{
row: 2,
type: BLOCKER_CHOCOLATE
}, {
row: 3,
type: BLOCKER_ICE
}, {
row: 4,
type: BLOCKER_CHOCOLATE
}, {
row: 5,
type: BLOCKER_ICE
}];
level.description = "Clear all blockers!\nZig-zag pattern challenge.";
} else if (i === 4) {
// Level 5: Score challenge
level.moves = movesCurve[i];
level.target = 1400;
level.blockers = [{
row: 2,
type: BLOCKER_CHOCOLATE
}, {
row: 3,
type: BLOCKER_ICE
}, {
row: 4,
type: BLOCKER_CHOCOLATE
}, {
row: 5,
type: BLOCKER_ICE
}, {
row: 6,
type: BLOCKER_CHOCOLATE
}];
level.description = "Score challenge!\nReach the target score to win.";
} else if (i === 5) {
// Level 6: Cross/diamond blockers
level.moves = movesCurve[i];
level.target = 1700;
level.blockers = [{
row: 0,
type: BLOCKER_CHOCOLATE
}, {
row: 4,
type: BLOCKER_ICE
}, {
row: 8,
type: BLOCKER_CHOCOLATE
}];
level.description = "Break all blockers!\nCross and diamond pattern.";
} else if (i === 6) {
// Level 7: U-shape blockers
level.moves = movesCurve[i];
level.target = 1900;
level.blockers = [{
row: 0,
type: BLOCKER_CHOCOLATE
}, {
row: 8,
type: BLOCKER_CHOCOLATE
}, {
row: 4,
type: BLOCKER_ICE
}, {
row: 7,
type: BLOCKER_ICE
}];
level.description = "Clear all blockers!\nU-shape at the bottom.";
} else if (i === 7) {
// Level 8: Vertical stripe blockers
level.moves = movesCurve[i];
level.target = 2100;
level.blockers = [{
row: 1,
type: BLOCKER_ICE
}, {
row: 3,
type: BLOCKER_CHOCOLATE
}, {
row: 5,
type: BLOCKER_ICE
}, {
row: 7,
type: BLOCKER_CHOCOLATE
}];
level.description = "Clear all blockers!\nVertical stripe in the center.";
} else if (i === 8) {
// Level 9: Spiral blockers
level.moves = movesCurve[i];
level.target = 2400;
level.blockers = [{
row: 0,
type: BLOCKER_ICE
}, {
row: 1,
type: BLOCKER_CHOCOLATE
}, {
row: 2,
type: BLOCKER_ICE
}, {
row: 3,
type: BLOCKER_CHOCOLATE
}, {
row: 4,
type: BLOCKER_ICE
}, {
row: 5,
type: BLOCKER_CHOCOLATE
}, {
row: 6,
type: BLOCKER_ICE
}, {
row: 7,
type: BLOCKER_CHOCOLATE
}];
level.description = "Clear all blockers!\nSpiral pattern.";
} else if (i === 9) {
// Level 10: Boss
level.moves = movesCurve[i];
level.target = 3000;
level.blockers = [{
row: 0,
type: BLOCKER_CHOCOLATE
}, {
row: 1,
type: BLOCKER_ICE
}, {
row: 2,
type: BLOCKER_CHOCOLATE
}, {
row: 3,
type: BLOCKER_ICE
}, {
row: 4,
type: BLOCKER_CHOCOLATE
}, {
row: 5,
type: BLOCKER_ICE
}, {
row: 6,
type: BLOCKER_CHOCOLATE
}, {
row: 7,
type: BLOCKER_ICE
}, {
row: 8,
type: BLOCKER_CHOCOLATE
}];
level.description = "Boss Level!\nReach the target score to win.";
} else {
// Levels 11-100: Increase difficulty, alternate win conditions, more blockers, less moves, higher targets
var base = levels[i % 10];
// Use movesCurve for main progression, but allow special bumps below
level.moves = movesCurve[i];
level.target = base.target + 400 * i;
// Copy blockers and add more for higher levels
level.blockers = [];
for (var b = 0; b < base.blockers.length; ++b) {
level.blockers.push({
row: base.blockers[b].row,
type: base.blockers[b].type
});
}
// Every 10th level: add extra blockers
if (i > 0 && i % 10 === 0) {
for (var r = 0; r < 3; ++r) {
level.blockers.push({
row: r,
type: i % 20 === 0 ? BLOCKER_CHOCOLATE : BLOCKER_ICE
});
}
level.description = "Super challenge!\nExtra blockers added.\nCan you win?";
// Give a small moves bump for every 10th level
level.moves += 1;
} else if (i > 0 && i % 5 === 0) {
// Every 5th level: Boss, higher target
level.target += 1000;
level.description = "Boss Level!\nBlockers everywhere.\nShow your skills!";
// Give a moves bump for boss
level.moves += 1;
} else if (i > 0 && i % 25 === 0) {
// Every 25th: Mega Boss
level.target += 2000;
level.moves = Math.max(5, level.moves - 2);
level.description = "Mega Boss!\nThe ultimate test.\nGood luck!";
} else if (i > 0 && i % 3 === 0) {
// Every 3rd: add random blocker row
var extraRow = randInt(0, BOARD_ROWS - 1);
var extraType = Math.random() < 0.5 ? BLOCKER_CHOCOLATE : BLOCKER_ICE;
level.blockers.push({
row: extraRow,
type: extraType
});
level.description = "Surprise row!\nExtra blockers appear.\nKeep matching!";
} else if (i > 0 && i % 7 === 0) {
// Every 7th: add two random blockers
for (var r = 0; r < 2; ++r) {
var randRow = randInt(0, BOARD_ROWS - 1);
var randType = Math.random() < 0.5 ? BLOCKER_CHOCOLATE : BLOCKER_ICE;
level.blockers.push({
row: randRow,
type: randType
});
}
level.description = "Blocker surprise!\nRandom blockers added.\nStay sharp!";
} else if (i > 0 && i % 13 === 0) {
// Every 13th: Lucky, more moves, higher target
level.moves += 3;
level.target += 1500;
level.description = "Lucky Level!\nMore moves, higher target.\nGo for it!";
} else {
// Default: inherit description
level.description = base.description;
}
// No winCondition property at all
}
levels.push(level);
}
// Always start at level 1 for every player/session
storage.currentLevel = 0;
var currentLevel = 0;
var movesLeft = typeof storage.movesLeft !== "undefined" ? storage.movesLeft : levels[currentLevel].moves;
var targetScore = levels[currentLevel].target;
var score = typeof storage.score !== "undefined" ? storage.score : 0;
var scoreTxt = null;
var movesTxt = null;
var targetTxt = null;
var boardContainer = null;
var matchQueue = [];
var refillQueue = [];
var isProcessing = false;
// --- Start Screen Overlay ---
var startScreen = new Container();
LK.gui.center.addChild(startScreen);
startScreen.visible = true;
// Background overlay (semi-transparent)
var startBg = LK.getAsset('blocker_ice', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
width: 1200,
height: 1600,
alpha: 0.85
});
startScreen.addChild(startBg);
// Game title
var titleTxt = new Text2('Candy Match Saga', {
size: 180,
fill: "#fff"
});
titleTxt.anchor.set(0.5, 0.5);
titleTxt.x = 0;
titleTxt.y = -400;
startScreen.addChild(titleTxt);
// Subtitle
var subtitleTxt = new Text2('Match candies, beat levels, have fun!', {
size: 70,
fill: "#fff"
});
subtitleTxt.anchor.set(0.5, 0.5);
subtitleTxt.x = 0;
subtitleTxt.y = -250;
startScreen.addChild(subtitleTxt);
// Play button
var playBtn = new Container();
var playBtnBg = LK.getAsset('start_button', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
width: 500,
height: 160
});
playBtn.addChild(playBtnBg);
// Removed the 'Start' label from the start screen play button
playBtn.anchorX = 0.5;
playBtn.anchorY = 0.5;
playBtn.x = 0;
playBtn.y = 200;
playBtn.interactive = true;
playBtn.buttonMode = true;
playBtn.down = function (x, y, obj) {
startScreen.visible = false;
// Show game UI
scoreTxt.visible = true;
movesTxt.visible = true;
targetTxt.visible = true;
levelTopRightTxt.visible = true;
bonusPanel.visible = true;
boardContainer.visible = true;
if (levelTxt) levelTxt.visible = true;
if (prevLevelBtn) prevLevelBtn.visible = true;
if (resetBtn) resetBtn.visible = true;
// Removed reference to nextLevelBtn which is not defined
if (window.levelDescTxt) window.levelDescTxt.visible = true;
if (counterPanel) counterPanel.visible = true;
};
startScreen.addChild(playBtn);
// Hide game UI until play is pressed
// GUI
scoreTxt = new Text2('Score: 0', {
size: 90,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
scoreTxt.visible = false;
movesTxt = new Text2('Moves: 20', {
size: 70,
fill: "#fff"
});
movesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(movesTxt);
movesTxt.y = 110;
movesTxt.visible = false;
targetTxt = new Text2('Target: 5000', {
size: 60,
fill: "#fff"
});
targetTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(targetTxt);
targetTxt.y = 180;
targetTxt.visible = false;
// Add level label to top right
var levelTopRightTxt = new Text2('Level: 1', {
size: 70,
fill: "#fff"
});
levelTopRightTxt.anchor.set(1, 0);
LK.gui.topRight.addChild(levelTopRightTxt);
levelTopRightTxt.x = 0;
levelTopRightTxt.y = 0;
levelTopRightTxt.visible = false;
// --- Bonus UI ---
var bonusPanel = new Container();
LK.gui.top.addChild(bonusPanel);
bonusPanel.x = 0;
bonusPanel.y = 250;
bonusPanel.visible = false;
// Track which bonus is selected (null, "bomb", etc)
var selectedBonus = null;
// Bomb bonus button
var bombBtnBg = LK.getAsset('candy_bomb', {
anchorX: 0.5,
anchorY: 0.5,
x: 120,
y: 0,
width: 140,
height: 140
});
bonusPanel.addChild(bombBtnBg);
var bombBtnLabel = new Text2('Bomb', {
size: 48,
fill: "#fff"
});
bombBtnLabel.anchor.set(0.5, 0);
bombBtnLabel.x = 120;
bombBtnLabel.y = 80;
bonusPanel.addChild(bombBtnLabel);
bombBtnBg.interactive = true;
bombBtnBg.buttonMode = true;
bombBtnBg.down = function (x, y, obj) {
selectedBonus = selectedBonus === "bomb" ? null : "bomb";
// Visual feedback
bombBtnBg.scaleX = bombBtnBg.scaleY = selectedBonus === "bomb" ? 1.2 : 1.0;
};
// Optionally, add more bonuses here in the future
// Board container
boardContainer = new Container();
game.addChild(boardContainer);
boardContainer.x = BOARD_OFFSET_X;
boardContainer.y = BOARD_OFFSET_Y;
boardContainer.visible = false;
// Initialize board
function initBoard() {
// Clear previous
for (var i = 0; i < candies.length; ++i) {
if (candies[i]) candies[i].destroyCandy();
}
candies = [];
board = [];
// Initialize objectives for this level
currentObjectives = getObjectivesForLevel(currentLevel);
currentObjectiveProgress = [];
for (var i = 0; i < currentObjectives.length; ++i) currentObjectiveProgress[i] = 0;
progressPanel.visible = true;
updateProgressPanel();
starPanel.visible = true;
updateStarPanel();
updateCounterPanel();
timedBombs = [];
conveyorActive = currentLevel >= 5; // Enable conveyor on level 6+
// --- Ensure all required assets for objectives are present in the board ---
// Build a list of required asset counts for this level's objectives
var requiredAssets = {};
for (var i = 0; i < currentObjectives.length; ++i) {
var obj = currentObjectives[i];
// Only count collect/clear objectives for mechanical assets
if (obj.asset) {
requiredAssets[obj.asset] = (requiredAssets[obj.asset] || 0) + obj.count;
}
}
// Track how many of each asset we have placed
var placedAssets = {};
for (var k in requiredAssets) placedAssets[k] = 0;
for (var row = 0; row < BOARD_ROWS; ++row) {
board[row] = [];
for (var col = 0; col < BOARD_COLS; ++col) {
var candy = new Candy();
candy.row = row;
candy.col = col;
candy.x = col * CELL_SIZE + CELL_SIZE / 2;
candy.y = row * CELL_SIZE + CELL_SIZE / 2;
// Unique blocker logic per level
var blockers = levels[currentLevel].blockers;
var placedBlocker = false;
// Guarantee at least one open path per row for passability
// We'll use a random openCol for each row, but keep it consistent for the row
var openCol = -1;
if (blockers.length > 0) {
// For each row, pick a random open column (or center for symmetry)
openCol = Math.floor(BOARD_COLS / 2);
// For more variety, you could use: openCol = randInt(0, BOARD_COLS - 1);
}
for (var b = 0; b < blockers.length; ++b) {
if (row === blockers[b].row) {
// Level-specific patterns
// Level 5/10: checkerboard, but add a diagonal line of blockers for extra challenge
if ((currentLevel === 4 || currentLevel === 9) && ((row + col) % 2 === 0 || row === col)) {
// Always leave openCol open
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Level 6: cross pattern, but add a diamond shape in the center
else if (currentLevel === 5 && (col === Math.floor(BOARD_COLS / 2) || row === Math.floor(BOARD_ROWS / 2) || Math.abs(col - Math.floor(BOARD_COLS / 2)) === Math.abs(row - Math.floor(BOARD_ROWS / 2)) && Math.abs(col - Math.floor(BOARD_COLS / 2)) <= 2)) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Level 7: chocolate on sides, ice in center, but add blockers in a U shape at the bottom
else if (currentLevel === 6 && ((row === 0 || row === BOARD_ROWS - 1) && blockers[b].type === BLOCKER_CHOCOLATE && (col === 0 || col === BOARD_COLS - 1) || row >= BOARD_ROWS - 3 && (col === 0 || col === BOARD_COLS - 1 || row === BOARD_ROWS - 1 && col > 0 && col < BOARD_COLS - 1))) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Level 8: alternating rows, but add a vertical stripe in the center
else if (currentLevel === 7 && (row % 2 === b % 2 || col === Math.floor(BOARD_COLS / 2))) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Level 9: spiral (simulate with increasing rows), but add a spiral arm from the bottom right
else if (currentLevel === 8 && (col >= row && col < BOARD_COLS - row || row + col === BOARD_COLS + BOARD_ROWS - 2 - row && row > BOARD_ROWS / 2)) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Level 10: nearly every row blocked, but leave a zig-zag path open
else if (currentLevel === 9 && !((row + col) % 2 === 1 && col !== 0 && col !== BOARD_COLS - 1)) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
// Default: alternate blockers on even/odd columns for variety, but add a random chance for double blockers
else if (!placedBlocker && (blockers[b].type === BLOCKER_CHOCOLATE && col % 2 === 0 || blockers[b].type === BLOCKER_ICE && col % 2 === 1 || Math.random() < 0.07)) {
if (col !== openCol) {
candy.setType(candy.type, SPECIAL_NONE, blockers[b].type);
placedBlocker = true;
}
}
}
}
// --- Place required assets for objectives ---
// Only place if not a blocker
if (!placedBlocker) {
// Try to place required assets first, if not enough placed yet
var placed = false;
for (var assetName in requiredAssets) {
if (placedAssets[assetName] < requiredAssets[assetName]) {
// Place this asset as a candy type
candy.setType(assetName, SPECIAL_NONE, BLOCKER_NONE);
placedAssets[assetName]++;
placed = true;
break;
}
}
// If all required assets are placed, continue to spawn them randomly if they are part of objectives
if (!placed) {
// Build a list of all asset names that are required for objectives in this level
var objectiveAssetNames = [];
for (var assetName in requiredAssets) {
objectiveAssetNames.push(assetName);
}
// --- Set random proportions for each objective asset for this level ---
// Only generate once per board init
if (!window._objectiveAssetProportions || window._objectiveAssetProportionsLevel !== currentLevel) {
window._objectiveAssetProportions = {};
var total = 0;
for (var i = 0; i < objectiveAssetNames.length; ++i) {
// Give each asset a random weight between 1 and 100
var w = randInt(1, 100);
window._objectiveAssetProportions[objectiveAssetNames[i]] = w;
total += w;
}
// Normalize to probabilities (sum to 1)
for (var i = 0; i < objectiveAssetNames.length; ++i) {
var k = objectiveAssetNames[i];
window._objectiveAssetProportions[k] /= total;
}
window._objectiveAssetProportionsLevel = currentLevel;
}
// If there are any objective assets, pick one to spawn according to the random proportions
// Reduce probability to 25% to lower chance of self-exploding objective boxes
if (objectiveAssetNames.length > 0 && Math.random() < 0.25) {
// 25% chance to spawn an objective asset
// Weighted random pick
var r = Math.random();
var acc = 0;
var chosenAsset = objectiveAssetNames[0];
for (var i = 0; i < objectiveAssetNames.length; ++i) {
var k = objectiveAssetNames[i];
acc += window._objectiveAssetProportions[k];
if (r <= acc) {
chosenAsset = k;
break;
}
}
candy.setType(chosenAsset, SPECIAL_NONE, BLOCKER_NONE);
} else {
// Otherwise, use a random candy type
candy.setType(getRandomCandyType(), SPECIAL_NONE, BLOCKER_NONE);
}
}
}
// Always add candies to the top of the boardContainer to ensure correct z-ordering
boardContainer.addChild(candy);
if (boardContainer.children && boardContainer.children.length > 1) {
// Move the newly added candy to the top (end of children array)
boardContainer.removeChild(candy);
boardContainer.addChild(candy);
}
candies.push(candy);
board[row][col] = candy;
}
}
// Remove initial matches
removeInitialMatches();
// Ensure no 3-in-a-row/col at board creation
for (var row = 0; row < BOARD_ROWS; ++row) {
//{aU.1}
for (var col = 0; col < BOARD_COLS; ++col) {
//{aU.2}
// Only check non-blockers
if (board[row][col].blocker !== BLOCKER_NONE) continue; //{aU.3}
var forbiddenTypes = {};
// Check left
if (col >= 2 && board[row][col - 1].type === board[row][col - 2].type) {
forbiddenTypes[board[row][col - 1].type] = true; //{aU.4}
}
// Check up
if (row >= 2 && board[row - 1][col].type === board[row - 2][col].type) {
forbiddenTypes[board[row - 1][col].type] = true; //{aU.5}
}
// If current type is forbidden, reroll
var tries = 0;
while (forbiddenTypes[board[row][col].type] && tries < 10) {
//{aU.6}
var newType = getRandomCandyType();
// Avoid forbidden types
while (forbiddenTypes[newType] && tries < 10) {
newType = getRandomCandyType();
tries++;
}
board[row][col].setType(newType);
tries++;
}
} //{aU.7}
} //{aU.8}
}
// Remove initial matches to avoid auto-matches at start
function removeInitialMatches() {
for (var row = 0; row < BOARD_ROWS; ++row) {
for (var col = 0; col < BOARD_COLS; ++col) {
// Only check non-blockers
if (board[row][col].blocker !== BLOCKER_NONE) continue; //{aW.1}
var changed = false;
var tries = 0;
do {
var type = board[row][col].type;
var hasRowMatch = col >= 2 && board[row][col - 1].type === type && board[row][col - 2].type === type;
var hasColMatch = row >= 2 && board[row - 1][col].type === type && board[row - 2][col].type === type;
if (hasRowMatch || hasColMatch) {
var forbiddenTypes = {};
if (hasRowMatch) forbiddenTypes[board[row][col - 1].type] = true;
if (hasColMatch) forbiddenTypes[board[row - 1][col].type] = true;
var newType = getRandomCandyType();
while ((forbiddenTypes[newType] || newType === type) && tries < 10) {
newType = getRandomCandyType();
tries++;
}
board[row][col].setType(newType);
changed = true;
} else {
changed = false;
}
tries++;
} while (changed && tries < 10);
}
}
}
// Get candy at board position
function getCandyAt(row, col) {
if (row < 0 || row >= BOARD_ROWS || col < 0 || col >= BOARD_COLS) return null;
return board[row][col];
}
// Swap two candies
function swapCandies(c1, c2, cb) {
swapping = true;
var r1 = c1.row,
c1c = c1.col,
r2 = c2.row,
c2c = c2.col;
// Swap in board
board[r1][c1c] = c2;
board[r2][c2c] = c1;
// Swap row/col
var tmpRow = c1.row,
tmpCol = c1.col;
c1.row = r2;
c1.col = c2c;
c2.row = r1;
c2.col = c1c;
// Ensure board and candies are in sync after swap
board[c1.row][c1.col] = c1;
board[c2.row][c2.col] = c2;
// Animate
var done = 0;
c1.moveTo(c1.col * CELL_SIZE + CELL_SIZE / 2, c1.row * CELL_SIZE + CELL_SIZE / 2, 180, function () {
done++;
if (done === 2 && cb) {
swapping = false;
cb();
}
});
c2.moveTo(c2.col * CELL_SIZE + CELL_SIZE / 2, c2.row * CELL_SIZE + CELL_SIZE / 2, 180, function () {
done++;
if (done === 2 && cb) {
swapping = false;
cb();
}
});
LK.getSound('swap').play();
}
// Check if two candies are adjacent
function areAdjacent(c1, c2) {
var dr = Math.abs(c1.row - c2.row);
var dc = Math.abs(c1.col - c2.col);
// Allow swapping in all four directions (up, down, left, right)
return dr === 1 && dc === 0 || dr === 0 && dc === 1;
}
// Find all matches on the board, including special candy creation
function findMatches() {
var matches = [];
var matchGroups = []; // For special candy creation
// Horizontal
for (var row = 0; row < BOARD_ROWS; ++row) {
var count = 1;
var startCol = 0;
for (var col = 1; col < BOARD_COLS; ++col) {
var prev = board[row][col - 1];
var curr = board[row][col];
if (curr.type === prev.type && curr.blocker === BLOCKER_NONE && prev.blocker === BLOCKER_NONE) {
count++;
} else {
if (count >= 3) {
var group = [];
for (var k = 0; k < count; ++k) {
group.push(board[row][col - 1 - k]);
}
matches = matches.concat(group);
matchGroups.push(group);
}
count = 1;
startCol = col;
}
}
if (count >= 3) {
var group = [];
for (var k = 0; k < count; ++k) {
group.push(board[row][BOARD_COLS - 1 - k]);
}
matches = matches.concat(group);
matchGroups.push(group);
}
}
// Vertical
for (var col = 0; col < BOARD_COLS; ++col) {
var count = 1;
var startRow = 0;
for (var row = 1; row < BOARD_ROWS; ++row) {
var prev = board[row - 1][col];
var curr = board[row][col];
if (curr.type === prev.type && curr.blocker === BLOCKER_NONE && prev.blocker === BLOCKER_NONE) {
count++;
} else {
if (count >= 3) {
var group = [];
for (var k = 0; k < count; ++k) {
group.push(board[row - 1 - k][col]);
}
matches = matches.concat(group);
matchGroups.push(group);
}
count = 1;
startRow = row;
}
}
if (count >= 3) {
var group = [];
for (var k = 0; k < count; ++k) {
group.push(board[BOARD_ROWS - 1 - k][col]);
}
matches = matches.concat(group);
matchGroups.push(group);
}
}
// Detect 2x2 square matches and mark for bomb
for (var row = 0; row < BOARD_ROWS - 1; ++row) {
for (var col = 0; col < BOARD_COLS - 1; ++col) {
var c1 = board[row][col];
var c2 = board[row][col + 1];
var c3 = board[row + 1][col];
var c4 = board[row + 1][col + 1];
if (c1.type === c2.type && c1.type === c3.type && c1.type === c4.type && c1.blocker === BLOCKER_NONE && c2.blocker === BLOCKER_NONE && c3.blocker === BLOCKER_NONE && c4.blocker === BLOCKER_NONE) {
// Only add if not already in a match group for this square
var alreadyMatched = false;
for (var mg = 0; mg < matchGroups.length; ++mg) {
var g = matchGroups[mg];
if (g.indexOf(c1) !== -1 && g.indexOf(c2) !== -1 && g.indexOf(c3) !== -1 && g.indexOf(c4) !== -1) {
alreadyMatched = true;
break;
}
}
if (!alreadyMatched) {
// Mark the top-left as bomb, others as normal
c1.special = SPECIAL_BOMB;
c1.setType(c1.type, SPECIAL_BOMB, c1.blocker);
// Remove c1 from matches so it is not destroyed, but destroy the other 3
var group = [c2, c3, c4];
matches = matches.concat(group);
matchGroups.push([c1, c2, c3, c4]);
// Store a flag for c1 to trigger a special 2x2 bomb explosion in removeMatches
c1._pendingSquareBomb = true;
}
}
}
}
// Remove duplicates
var unique = [];
for (var i = 0; i < matches.length; ++i) {
if (unique.indexOf(matches[i]) === -1) unique.push(matches[i]);
}
// Mark special candy creation (striped, bomb, rainbow)
for (var g = 0; g < matchGroups.length; ++g) {
var group = matchGroups[g];
if (group.length === 4) {
// Striped candy: horizontal or vertical
var isHorizontal = group[0].row === group[1].row;
var specialCandy = group[1]; // Place special at second in group
if (specialCandy.special === SPECIAL_NONE) {
specialCandy.special = SPECIAL_STRIPED;
specialCandy.setType(specialCandy.type, SPECIAL_STRIPED, specialCandy.blocker);
// Mark orientation for correct effect
specialCandy._stripedOrientation = isHorizontal ? "horizontal" : "vertical";
}
// Remove the special candy from the group so it is not destroyed
for (var i = 0; i < group.length; ++i) {
if (group[i] === specialCandy) {
group.splice(i, 1);
break;
}
}
}
if (group.length === 5) {
// Color bomb (rainbow) only for exactly 5-in-a-row
var specialCandy = group[Math.floor(group.length / 2)]; // Place in the middle
if (specialCandy.special === SPECIAL_NONE) {
specialCandy.special = SPECIAL_RAINBOW;
specialCandy.setType(specialCandy.type, SPECIAL_RAINBOW, specialCandy.blocker);
}
// Remove the special candy from the group so it is not destroyed
for (var i = 0; i < group.length; ++i) {
if (group[i] === specialCandy) {
group.splice(i, 1);
break;
}
}
}
}
// Bomb candy for T or L shape
// Find intersections of horizontal and vertical matches
for (var i = 0; i < matchGroups.length; ++i) {
var groupA = matchGroups[i];
if (groupA.length !== 3) continue;
for (var j = i + 1; j < matchGroups.length; ++j) {
var groupB = matchGroups[j];
if (groupB.length !== 3) continue;
// Check for intersection
for (var a = 0; a < 3; ++a) {
for (var b = 0; b < 3; ++b) {
if (groupA[a] === groupB[b]) {
// Place bomb at intersection
var bombCandy = groupA[a];
if (bombCandy.special === SPECIAL_NONE) {
bombCandy.special = SPECIAL_BOMB;
bombCandy.setType(bombCandy.type, SPECIAL_BOMB, bombCandy.blocker);
}
}
}
}
}
}
return unique;
}
// Remove matched candies and animate, including special candy activation and blockers
function removeMatches(matches, cb) {
if (!matches || matches.length === 0) {
if (cb) cb();
return;
}
LK.getSound('match').play();
var done = 0;
var toRemove = [];
// Activate special candies in matches
for (var i = 0; i < matches.length; ++i) {
var candy = matches[i];
if (candy.special === SPECIAL_STRIPED) {
// Striped candy: clear row if created from horizontal match, column if from vertical
// Determine orientation by checking if the special was created from a horizontal or vertical match
// We'll use a property set during match detection, or fallback to random if not set (legacy)
var isHorizontal = false;
if (typeof candy._stripedOrientation !== "undefined") {
isHorizontal = candy._stripedOrientation === "horizontal";
} else {
// Fallback: random (legacy, but should not happen with new match logic)
isHorizontal = Math.random() < 0.5;
}
if (isHorizontal) {
// Clear row
for (var c = 0; c < BOARD_COLS; ++c) {
var target = board[candy.row][c];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
} else {
// Clear column
for (var r = 0; r < BOARD_ROWS; ++r) {
var target = board[r][candy.col];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
} else if (candy.special === SPECIAL_RAINBOW) {
// Clear all candies of a random type on board
var colorType = getRandomCandyType();
for (var r = 0; r < BOARD_ROWS; ++r) {
for (var c = 0; c < BOARD_COLS; ++c) {
var target = board[r][c];
if (target.type === colorType && toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
} else if (candy.special === SPECIAL_BOMB) {
// If this bomb was created by a 2x2 square match, do a shine and destroy 8 neighbors
if (candy._pendingSquareBomb) {
// Shine effect: flash the bomb candy
LK.effects.flashObject(candy, 0xffff00, 400);
// Destroy 8 neighbors (not self)
for (var dr = -1; dr <= 1; ++dr) {
for (var dc = -1; dc <= 1; ++dc) {
var rr = candy.row + dr,
cc = candy.col + dc;
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
if (rr === candy.row && cc === candy.col) continue; // skip self
var target = board[rr][cc];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
}
// Remove the flag so it doesn't trigger again
delete candy._pendingSquareBomb;
} else {
// Default bomb: Clear 3x3 area (including self)
for (var dr = -1; dr <= 1; ++dr) {
for (var dc = -1; dc <= 1; ++dc) {
var rr = candy.row + dr,
cc = candy.col + dc;
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
var target = board[rr][cc];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
}
}
} else {
if (toRemove.indexOf(candy) === -1) toRemove.push(candy);
}
}
// Remove blockers if matched
for (var i = 0; i < toRemove.length; ++i) {
var candy = toRemove[i];
if (candy.blocker === BLOCKER_CHOCOLATE) {
// Remove chocolate in one match
candy.blocker = BLOCKER_NONE;
candy.setType(candy.type, candy.special, BLOCKER_NONE);
continue;
}
if (candy.blocker === BLOCKER_ICE) {
// Remove ice in two matches: first match cracks, second removes
if (!candy._iceCracked) {
candy._iceCracked = true;
// Tint or visually indicate cracked ice (optional)
candy.setType(candy.type, candy.special, BLOCKER_ICE);
// Do not mark as matched, so it remains for the next match
continue;
} else {
candy.blocker = BLOCKER_NONE;
delete candy._iceCracked;
candy.setType(candy.type, candy.special, BLOCKER_NONE);
// Now allow to be matched and removed
continue;
}
}
candy.isMatched = true;
// Animate scale down and fade
tween(candy, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function (candy) {
return function () {
if (candy.asset) {
candy.removeChild(candy.asset);
candy.asset.destroy();
candy.asset = null;
}
candy.visible = false;
done++;
if (done === toRemove.length && cb) cb();
};
}(candy)
});
// Add score
score += 100;
}
}
// Drop candies to fill empty spaces
function dropCandies(cb) {
var moved = false;
for (var col = 0; col < BOARD_COLS; ++col) {
for (var row = BOARD_ROWS - 1; row >= 0; --row) {
var candy = board[row][col];
if (!candy.isMatched && candy.visible) continue;
// Find nearest above
for (var above = row - 1; above >= 0; --above) {
var aboveCandy = board[above][col];
if (!aboveCandy.isMatched && aboveCandy.visible) {
// Only move if the target cell is not already occupied by a visible, non-matched candy
if (!board[row][col].visible || board[row][col].isMatched) {
// Only allow candies to fall if there is a candy above (do not close the top of the column)
board[row][col] = aboveCandy;
aboveCandy.row = row;
aboveCandy.col = col;
aboveCandy.moveTo(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 180);
board[above][col] = candy;
moved = true;
}
break;
}
}
// Do not close the top of the column: if there is no candy above, leave the cell empty for refillBoard
}
}
if (cb) LK.setTimeout(cb, moved ? 200 : 0);
}
// Fill empty spaces with new candies
function refillBoard(cb) {
var created = false;
for (var col = 0; col < BOARD_COLS; ++col) {
for (var row = 0; row < BOARD_ROWS; ++row) {
var candy = board[row][col];
if (!candy.isMatched && candy.visible) continue;
// Only create a new candy if the cell is not already occupied by a visible, non-matched candy
if (!board[row][col].visible || board[row][col].isMatched) {
// Create new candy
var newCandy = new Candy();
newCandy.row = row;
newCandy.col = col;
newCandy.x = col * CELL_SIZE + CELL_SIZE / 2;
newCandy.y = -CELL_SIZE + CELL_SIZE / 2;
// Build a list of all asset names that are required for objectives in this level
var objectiveAssetNames = [];
for (var i = 0; i < currentObjectives.length; ++i) {
if (currentObjectives[i].asset) {
if (objectiveAssetNames.indexOf(currentObjectives[i].asset) === -1) {
objectiveAssetNames.push(currentObjectives[i].asset);
}
}
}
// If there are any objective assets, randomly pick one to spawn with lower probability
// Reduce probability to 25% to lower chance of self-exploding objective boxes
if (objectiveAssetNames.length > 0 && Math.random() < 0.25) {
// --- Set random proportions for each objective asset for this level (reuse from initBoard) ---
if (!window._objectiveAssetProportions || window._objectiveAssetProportionsLevel !== currentLevel) {
window._objectiveAssetProportions = {};
var total = 0;
for (var i = 0; i < objectiveAssetNames.length; ++i) {
// Give each asset a random weight between 1 and 100
var w = randInt(1, 100);
window._objectiveAssetProportions[objectiveAssetNames[i]] = w;
total += w;
}
// Normalize to probabilities (sum to 1)
for (var i = 0; i < objectiveAssetNames.length; ++i) {
var k = objectiveAssetNames[i];
window._objectiveAssetProportions[k] /= total;
}
window._objectiveAssetProportionsLevel = currentLevel;
}
// Weighted random pick
var r = Math.random();
var acc = 0;
var chosenAsset = objectiveAssetNames[0];
for (var i = 0; i < objectiveAssetNames.length; ++i) {
var k = objectiveAssetNames[i];
acc += window._objectiveAssetProportions[k];
if (r <= acc) {
chosenAsset = k;
break;
}
}
newCandy.setType(chosenAsset, SPECIAL_NONE, BLOCKER_NONE);
} else {
// Otherwise, use a random candy type
newCandy.setType(getRandomCandyType(), SPECIAL_NONE, BLOCKER_NONE);
}
// Always add new candies to the top of the boardContainer to ensure correct z-ordering
boardContainer.addChild(newCandy);
if (boardContainer.children && boardContainer.children.length > 1) {
// Move the newly added candy to the top (end of children array)
boardContainer.removeChild(newCandy);
boardContainer.addChild(newCandy);
}
candies.push(newCandy);
board[row][col] = newCandy;
newCandy.moveTo(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, 220);
created = true;
}
}
}
if (cb) LK.setTimeout(cb, created ? 220 : 0);
}
// Remove matched candies from board
function clearMatchedCandies() {
for (var row = 0; row < BOARD_ROWS; ++row) {
for (var col = 0; col < BOARD_COLS; ++col) {
var candy = board[row][col];
if (candy.isMatched) {
if (candy._iceCracked) {
delete candy._iceCracked;
}
candy.destroyCandy();
// Remove from candies array
for (var i = 0; i < candies.length; ++i) {
if (candies[i] === candy) {
candies.splice(i, 1);
break;
}
}
// Replace with dummy invisible candy for drop logic
var dummy = new Candy();
dummy.row = row;
dummy.col = col;
dummy.visible = false;
board[row][col] = dummy;
// Always add dummy candies to the bottom of the boardContainer so they don't cover visible candies
boardContainer.addChildAt(dummy, 0);
}
}
}
}
// Deselect all candies
function deselectAll() {
for (var i = 0; i < candies.length; ++i) {
candies[i].setSelected(false);
}
selectedCandy = null;
}
// Handle user tap
function handleTap(x, y, obj) {
if (swapping || animating || isProcessing) return;
// Convert to board coordinates, ensuring correct offset and boundaries
// Remove any offset miscalculations: boardContainer.x/y is already set, so local is relative to grid origin
// For touch/click, always use the event's x/y relative to the game, then subtract boardContainer.x/y to get local grid coordinates
var localX = x - boardContainer.x;
var localY = y - boardContainer.y;
// Clamp localX/localY to be within the grid area
if (localX < 0 || localY < 0) return;
var col = Math.floor(localX / CELL_SIZE);
var row = Math.floor(localY / CELL_SIZE);
// Clamp to grid bounds
// Boundary check
if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return;
var candy = board[row][col];
// Debug: Draw a highlight rectangle over the selected cell
if (typeof handleTap._debugRect !== "undefined" && handleTap._debugRect) {
boardContainer.removeChild(handleTap._debugRect);
handleTap._debugRect.destroy();
handleTap._debugRect = null;
}
var debugRect = LK.getAsset('blocker_ice', {
anchorX: 0,
anchorY: 0,
x: col * CELL_SIZE,
y: row * CELL_SIZE,
width: CELL_SIZE,
height: CELL_SIZE,
alpha: 0.25
});
// Insert debugRect at the bottom of boardContainer's children so it doesn't block candy input
if (boardContainer.children && boardContainer.children.length > 0) {
boardContainer.addChildAt(debugRect, 0);
} else {
boardContainer.addChild(debugRect);
}
handleTap._debugRect = debugRect;
if (!candy || candy.blocker !== BLOCKER_NONE) return;
// --- Bonus: Bomb usage ---
if (selectedBonus === "bomb") {
// Use bomb on this cell: destroy up to 12 tiles in a cross pattern (center, 4 orthogonal, 4 diagonal, and 3 more in a star/cross)
var toRemove = [];
var bombPattern = [[0, 0],
// center
[-1, 0], [1, 0], [0, -1], [0, 1],
// orthogonal
[-1, -1], [-1, 1], [1, -1], [1, 1],
// diagonal
[-2, 0], [2, 0], [0, -2], [0, 2] // extended cross
];
for (var i = 0; i < bombPattern.length; ++i) {
var dr = bombPattern[i][0];
var dc = bombPattern[i][1];
var rr = row + dr;
var cc = col + dc;
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
var target = board[rr][cc];
if (target && target.blocker === BLOCKER_NONE && toRemove.indexOf(target) === -1) {
toRemove.push(target);
}
}
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--; // Using a bonus still costs a move
if (movesLeft < 0) movesLeft = 0;
if (movesLeft < 0) movesLeft = 0;
updateGUI();
selectedBonus = null;
bombBtnBg.scaleX = bombBtnBg.scaleY = 1.0;
deselectAll();
return;
}
// If nothing selected, select this candy
if (!selectedCandy) {
deselectAll();
selectedCandy = candy;
candy.setSelected(true);
return;
}
// If clicking the same candy, deselect
if (selectedCandy === candy) {
deselectAll();
return;
}
// If adjacent, try to swap
if (areAdjacent(selectedCandy, candy)) {
swapping = true;
selectedCandy.setSelected(false);
candy.setSelected(false);
// Lock input during animation
var c1 = selectedCandy;
var c2 = candy;
deselectAll();
swapCandies(c1, c2, function () {
// Special candy swap logic
var specialActivated = false;
// Rainbow (color bomb) swap
if (c1.special === SPECIAL_RAINBOW || c2.special === SPECIAL_RAINBOW) {
var colorType = c1.special === SPECIAL_RAINBOW ? c2.type : c1.type;
var toRemove = [];
for (var r = 0; r < BOARD_ROWS; ++r) {
for (var c = 0; c < BOARD_COLS; ++c) {
var target = board[r][c];
if (target.type === colorType) toRemove.push(target);
}
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
}
// Striped + striped: clear row and col
else if (c1.special === SPECIAL_STRIPED && c2.special === SPECIAL_STRIPED) {
var toRemove = [];
for (var c = 0; c < BOARD_COLS; ++c) {
var t1 = board[c1.row][c];
if (toRemove.indexOf(t1) === -1) toRemove.push(t1);
}
for (var r = 0; r < BOARD_ROWS; ++r) {
var t2 = board[r][c2.col];
if (toRemove.indexOf(t2) === -1) toRemove.push(t2);
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
}
// Bomb + any: clear 3x3 around both
else if (c1.special === SPECIAL_BOMB || c2.special === SPECIAL_BOMB) {
var toRemove = [];
var bombCandies = [c1, c2];
for (var b = 0; b < 2; ++b) {
if (bombCandies[b].special === SPECIAL_BOMB) {
for (var dr = -1; dr <= 1; ++dr) {
for (var dc = -1; dc <= 1; ++dc) {
var rr = bombCandies[b].row + dr,
cc = bombCandies[b].col + dc;
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
var target = board[rr][cc];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
}
}
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
}
if (!specialActivated) {
// After swap, check for matches
var matches = findMatches();
if (matches.length > 0) {
processMatches();
movesLeft--;
if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
} else {
// No match, swap back
LK.getSound('fail').play();
// Animate a quick shake for both candies
var origX1 = c1.col * CELL_SIZE + CELL_SIZE / 2;
var origY1 = c1.row * CELL_SIZE + CELL_SIZE / 2;
var origX2 = c2.col * CELL_SIZE + CELL_SIZE / 2;
var origY2 = c2.row * CELL_SIZE + CELL_SIZE / 2;
tween(c1, {
x: origX1 + 20
}, {
duration: 60,
onFinish: function onFinish() {
tween(c1, {
x: origX1
}, {
duration: 60
});
}
});
tween(c2, {
x: origX2 - 20
}, {
duration: 60,
onFinish: function onFinish() {
tween(c2, {
x: origX2
}, {
duration: 60
});
}
});
// Actually swap back the candies to their original positions in the board and update their row/col
swapCandies(c1, c2, function () {
swapping = false;
deselectAll();
// Re-select the original candy for user feedback
selectedCandy = c1;
c1.setSelected(true);
});
movesLeft--;
if (movesLeft < 0) movesLeft = 0;
updateGUI();
}
}
});
return;
}
// Not adjacent, select new candy
deselectAll();
selectedCandy = candy;
candy.setSelected(true);
}
// Process matches and refill
function processMatches() {
isProcessing = true;
var matches = findMatches();
if (matches.length === 0) {
swapping = false;
isProcessing = false;
// Defensive: ensure isProcessing is false after resetBtn
isProcessing = false;
deselectAll();
// Save progress after move
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
checkGameEnd();
return;
}
removeMatches(matches, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
// --- Update objective progress after all matches/refills ---
for (var i = 0; i < currentObjectives.length; ++i) {
var obj = currentObjectives[i];
if (obj.type === OBJECTIVE_COLLECT) {
// Count how many of this mechanical asset were matched (destroyed) this turn
var matched = 0;
for (var j = 0; j < matches.length; ++j) {
if (matches[j].type === obj.asset && matches[j].isMatched) {
matched++;
} else if (matches[j].isMatched && matches[j].type && obj.asset && matches[j].type !== obj.asset) {
// Error log if a match is counted for the wrong asset
console.error("Objective mismatch: tried to count " + matches[j].type + " for " + obj.asset);
}
}
currentObjectiveProgress[i] += matched;
} else if (obj.type === OBJECTIVE_CLEAR) {
// Count blockers destroyed (match asset to correct blocker asset)
var cleared = 0;
for (var j = 0; j < matches.length; ++j) {
// Only count if the matched candy/blocker was actually destroyed and matches the correct asset
if (matches[j].isMatched && obj.asset) {
// Map blocker type to asset name
var blockerAsset = '';
if (matches[j].blocker === BLOCKER_CHOCOLATE) blockerAsset = 'blocker_chocolate';
if (matches[j].blocker === BLOCKER_ICE) blockerAsset = 'blocker_ice';
// Only count if the asset matches the objective asset
if (blockerAsset === obj.asset) {
cleared++;
}
}
}
currentObjectiveProgress[i] += cleared;
} else if (obj.type === OBJECTIVE_RESCUE) {
// Not used in mechanical theme, but left for extensibility
var rescued = 0;
for (var j = 0; j < matches.length; ++j) {
if (matches[j].special === SPECIAL_BOMB && matches[j].row === BOARD_ROWS - 1 && matches[j].isMatched) rescued++;
}
currentObjectiveProgress[i] += rescued;
}
// Clamp to max
if (currentObjectiveProgress[i] > obj.count) currentObjectiveProgress[i] = obj.count;
}
updateProgressPanel();
updateStarPanel();
updateCounterPanel();
// --- Timed Bombs: decrement and check for explosion ---
for (var t = timedBombs.length - 1; t >= 0; --t) {
timedBombs[t].movesLeft--;
if (timedBombs[t].movesLeft <= 0) {
// Bomb explodes: game over
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
isProcessing = false;
// Defensive: ensure isProcessing is false after timed bomb explosion
isProcessing = false;
LK.setTimeout(function () {
// Reset current level: reset score, moves, board
score = 0;
// Always reset movesLeft to the initial moves for this level, never negative
movesLeft = Math.max(0, levels[currentLevel].moves);
targetScore = levels[currentLevel].target;
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
updateGUI();
initBoard();
updateCounterPanel();
isProcessing = false;
}, 1200); // Wait for game over animation to finish before restarting
return;
}
}
// --- Conveyor: shift candies if active ---
if (conveyorActive) {
shiftConveyor();
}
// Chocolate spread: after all moves, if any chocolate exists, spread to adjacent
var chocolateList = [];
for (var row = 0; row < BOARD_ROWS; ++row) {
for (var col = 0; col < BOARD_COLS; ++col) {
var c = board[row][col];
if (c.blocker === BLOCKER_CHOCOLATE) chocolateList.push(c);
}
}
if (chocolateList.length > 0) {
// Try to spread chocolate to adjacent non-blocker, non-special, non-chocolate
for (var i = 0; i < chocolateList.length; ++i) {
var c = chocolateList[i];
var dirs = [[0, 1], [1, 0], [0, -1], [-1, 0]];
for (var d = 0; d < dirs.length; ++d) {
var rr = c.row + dirs[d][0],
cc = c.col + dirs[d][1];
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
var target = board[rr][cc];
if (target.blocker === BLOCKER_NONE && target.special === SPECIAL_NONE) {
target.blocker = BLOCKER_CHOCOLATE;
target.setType(target.type, target.special, BLOCKER_CHOCOLATE);
break;
}
}
}
}
}
// After all refills, check for new matches
var newMatches = findMatches();
if (newMatches.length > 0) {
// Defensive: reset isProcessing before recursion to avoid freeze if processMatches is called recursively
isProcessing = false;
processMatches();
return; // Prevent further code execution after recursion
} else {
swapping = false;
isProcessing = false;
deselectAll();
// Save progress after move
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
// --- Check for win/lose conditions ---
if (allObjectivesComplete()) {
// Celebrate: flash screen, animate stars, etc.
for (var s = 0; s < 3; ++s) {
if (starIcons[s].alpha === 1) LK.effects.flashObject(starIcons[s], 0xffff00, 800);
}
LK.effects.flashScreen(0x00ff00, 1000);
// Show next level screen after passing the chapter
isProcessing = false;
// Defensive: ensure isProcessing is false after win
isProcessing = false;
LK.setTimeout(function () {
// Advance to next level if not at last level
if (currentLevel < levels.length - 1) {
currentLevel++;
} else {
// If at last level, loop to first or show a "congratulations" message
currentLevel = 0;
}
// Reset score, moves, and target for new level
score = 0;
movesLeft = levels[currentLevel].moves;
targetScore = levels[currentLevel].target;
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
updateGUI();
initBoard();
updateCounterPanel();
// Show a "Next Level" overlay
if (typeof window.nextLevelScreen === "undefined") {
window.nextLevelScreen = new Container();
LK.gui.center.addChild(window.nextLevelScreen);
window.nextLevelScreen.visible = false;
// Background
var nextBg = LK.getAsset('blocker_ice', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
width: 1200,
height: 1200,
alpha: 0.85
});
window.nextLevelScreen.addChild(nextBg);
// Title
var nextTitle = new Text2('Next Level!', {
size: 160,
fill: "#fff"
});
nextTitle.anchor.set(0.5, 0.5);
nextTitle.x = 0;
nextTitle.y = -200;
window.nextLevelScreen.addChild(nextTitle);
// Level number
window.nextLevelScreen.levelNumTxt = new Text2('', {
size: 100,
fill: "#fff"
});
window.nextLevelScreen.levelNumTxt.anchor.set(0.5, 0.5);
window.nextLevelScreen.levelNumTxt.x = 0;
window.nextLevelScreen.levelNumTxt.y = 0;
window.nextLevelScreen.addChild(window.nextLevelScreen.levelNumTxt);
// Play button
var nextPlayBtn = new Container();
var nextPlayBtnBg = LK.getAsset('start_button', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
width: 500,
height: 160
});
nextPlayBtn.addChild(nextPlayBtnBg);
nextPlayBtn.anchorX = 0.5;
nextPlayBtn.anchorY = 0.5;
nextPlayBtn.x = 0;
nextPlayBtn.y = 250;
nextPlayBtn.interactive = true;
nextPlayBtn.buttonMode = true;
nextPlayBtn.down = function (x, y, obj) {
window.nextLevelScreen.visible = false;
// Show game UI
if (scoreTxt) scoreTxt.visible = true;
if (movesTxt) movesTxt.visible = true;
if (targetTxt) targetTxt.visible = true;
if (levelTopRightTxt) levelTopRightTxt.visible = true;
if (bonusPanel) bonusPanel.visible = true;
if (boardContainer) boardContainer.visible = true;
if (levelTxt) levelTxt.visible = true;
if (prevLevelBtn) prevLevelBtn.visible = true;
if (resetBtn) resetBtn.visible = true;
if (window.levelDescTxt) window.levelDescTxt.visible = true;
if (counterPanel) counterPanel.visible = true;
};
window.nextLevelScreen.addChild(nextPlayBtn);
}
// Update level number
window.nextLevelScreen.levelNumTxt.setText('Level: ' + (currentLevel + 1));
window.nextLevelScreen.visible = true;
// Hide game UI while next level screen is visible
if (scoreTxt) scoreTxt.visible = false;
if (movesTxt) movesTxt.visible = false;
if (targetTxt) targetTxt.visible = false;
if (levelTopRightTxt) levelTopRightTxt.visible = false;
if (bonusPanel) bonusPanel.visible = false;
if (boardContainer) boardContainer.visible = false;
if (levelTxt) levelTxt.visible = false;
if (prevLevelBtn) prevLevelBtn.visible = false;
if (resetBtn) resetBtn.visible = false;
if (window.levelDescTxt) window.levelDescTxt.visible = false;
if (counterPanel) counterPanel.visible = false;
}, 1200); // Wait for win animation to finish before showing next level screen
return;
}
checkGameEnd();
}
});
});
});
}
// Update GUI
function updateGUI() {
scoreTxt.setText('Score: ' + score);
movesTxt.setText('Moves: ' + movesLeft);
targetTxt.setText('Target: ' + targetScore);
// Show "Boss" for every 5th level (level 5, 10, 15, ...)
var levelNum = currentLevel + 1;
if (levelNum % 5 === 0) {
levelTxt.setText('Level: ' + levelNum + ' (Boss!)');
if (typeof levelTopRightTxt !== "undefined") {
levelTopRightTxt.setText('Level: ' + levelNum + ' (Boss!)');
}
} else {
levelTxt.setText('Level: ' + levelNum);
if (typeof levelTopRightTxt !== "undefined") {
levelTopRightTxt.setText('Level: ' + levelNum);
}
}
// Show level description and pass requirement at the bottom center
if (!window.levelDescTxt) {
window.levelDescTxt = new Text2('', {
size: 60,
fill: "#fff"
});
window.levelDescTxt.anchor.set(0.5, 1);
LK.gui.bottom.addChild(window.levelDescTxt);
window.levelDescTxt.y = -100;
window.levelDescTxt.visible = false;
}
// Format description to be 3 lines below each other
var desc = levels[currentLevel].description || '';
var descLines = desc.split('\n');
while (descLines.length < 3) descLines.push('');
// Show objectives as requirements
for (var i = 0; i < currentObjectives.length; ++i) {
var obj = currentObjectives[i];
if (obj.type === OBJECTIVE_COLLECT) {
var assetName = obj.asset ? obj.asset.replace('candy_', '').replace('blocker_', '') : '';
descLines.push('Collect ' + obj.count + ' ' + assetName);
}
if (obj.type === OBJECTIVE_CLEAR) {
var assetName = obj.asset ? obj.asset.replace('candy_', '').replace('blocker_', '') : '';
descLines.push('Clear ' + obj.count + ' ' + assetName);
}
if (obj.type === OBJECTIVE_RESCUE) descLines.push('Rescue ' + obj.count + ' mechanics');
}
window.levelDescTxt.setText(descLines.join('\n'));
updateProgressPanel();
updateStarPanel();
updateCounterPanel();
}
// Check for win/lose
function checkGameEnd() {
// Game over if out of moves and not all objectives complete
if (movesLeft <= 0) {
if (allObjectivesComplete()) {
LK.showYouWin();
} else {
LK.showGameOver();
isProcessing = false;
// Defensive: ensure isProcessing is false after game over
isProcessing = false;
LK.setTimeout(function () {
// Reset current level: reset score, moves, board
score = 0;
// Always reset movesLeft to the initial moves for this level, never negative
movesLeft = Math.max(0, levels[currentLevel].moves);
targetScore = levels[currentLevel].target;
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
updateGUI();
initBoard();
updateCounterPanel();
isProcessing = false;
}, 1200); // Wait for game over animation to finish before restarting
}
}
}
// Game event handlers
game.down = function (x, y, obj) {
// Don't allow tap in top left 100x100
if (x < 100 && y < 100) return;
handleTap(x, y, obj);
};
// Drag-to-swap logic
var dragStartCandy = null;
var dragCurrentCandy = null;
var dragActive = false;
// Helper: get candy at pixel position (returns null if not valid)
function getCandyAtPixel(x, y) {
var localX = x - boardContainer.x;
var localY = y - boardContainer.y;
if (localX < 0 || localY < 0) return null;
var col = Math.floor(localX / CELL_SIZE);
var row = Math.floor(localY / CELL_SIZE);
if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return null;
return board[row][col];
}
// Override game.down for drag start
game.down = function (x, y, obj) {
// Don't allow tap in top left 100x100
if (x < 100 && y < 100) return;
// If bomb bonus is selected, allow placing bomb anywhere on the board
if (selectedBonus === "bomb") {
// Convert to board coordinates
var localX = x - boardContainer.x;
var localY = y - boardContainer.y;
if (localX < 0 || localY < 0) return;
var col = Math.floor(localX / CELL_SIZE);
var row = Math.floor(localY / CELL_SIZE);
if (col < 0 || col >= BOARD_COLS || row < 0 || row >= BOARD_ROWS) return;
var candy = board[row][col];
// Only allow bomb on non-blocker
if (!candy || candy.blocker !== BLOCKER_NONE) return;
// Use bomb on this cell: destroy up to 12 tiles in a cross pattern (center, 4 orthogonal, 4 diagonal, and 3 more in a star/cross)
var toRemove = [];
var bombPattern = [[0, 0], [-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1], [-2, 0], [2, 0], [0, -2], [0, 2]];
for (var i = 0; i < bombPattern.length; ++i) {
var dr = bombPattern[i][0];
var dc = bombPattern[i][1];
var rr = row + dr;
var cc = col + dc;
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
var target = board[rr][cc];
if (target && target.blocker === BLOCKER_NONE && toRemove.indexOf(target) === -1) {
toRemove.push(target);
}
}
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--; // Using a bonus still costs a move
if (movesLeft < 0) movesLeft = 0;
updateGUI();
selectedBonus = null;
bombBtnBg.scaleX = bombBtnBg.scaleY = 1.0;
deselectAll();
return;
}
if (swapping || animating || isProcessing) return;
var candy = getCandyAtPixel(x, y);
if (!candy || candy.blocker !== BLOCKER_NONE) return;
dragStartCandy = candy;
dragCurrentCandy = candy;
dragActive = true;
deselectAll();
candy.setSelected(true);
selectedCandy = candy;
};
// Drag move handler
game.move = function (x, y, obj) {
if (!dragActive || swapping || animating || isProcessing) return;
var candy = getCandyAtPixel(x, y);
if (!candy || candy.blocker !== BLOCKER_NONE) return;
if (candy === dragStartCandy) return;
// Only allow swap with adjacent
if (areAdjacent(dragStartCandy, candy)) {
// Lock drag
dragActive = false;
dragStartCandy.setSelected(false);
candy.setSelected(false);
var c1 = dragStartCandy;
var c2 = candy;
deselectAll();
swapCandies(c1, c2, function () {
// Special candy swap logic (same as tap)
var specialActivated = false;
if (c1.special === SPECIAL_RAINBOW || c2.special === SPECIAL_RAINBOW) {
var colorType = c1.special === SPECIAL_RAINBOW ? c2.type : c1.type;
var toRemove = [];
for (var r = 0; r < BOARD_ROWS; ++r) {
for (var c = 0; c < BOARD_COLS; ++c) {
var target = board[r][c];
if (target.type === colorType) toRemove.push(target);
}
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
} else if (c1.special === SPECIAL_STRIPED && c2.special === SPECIAL_STRIPED) {
var toRemove = [];
for (var c = 0; c < BOARD_COLS; ++c) {
var t1 = board[c1.row][c];
if (toRemove.indexOf(t1) === -1) toRemove.push(t1);
}
for (var r = 0; r < BOARD_ROWS; ++r) {
var t2 = board[r][c2.col];
if (toRemove.indexOf(t2) === -1) toRemove.push(t2);
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
} else if (c1.special === SPECIAL_BOMB || c2.special === SPECIAL_BOMB) {
var toRemove = [];
var bombCandies = [c1, c2];
for (var b = 0; b < 2; ++b) {
if (bombCandies[b].special === SPECIAL_BOMB) {
for (var dr = -1; dr <= 1; ++dr) {
for (var dc = -1; dc <= 1; ++dc) {
var rr = bombCandies[b].row + dr,
cc = bombCandies[b].col + dc;
if (rr >= 0 && rr < BOARD_ROWS && cc >= 0 && cc < BOARD_COLS) {
var target = board[rr][cc];
if (toRemove.indexOf(target) === -1) toRemove.push(target);
}
}
}
}
}
removeMatches(toRemove, function () {
clearMatchedCandies();
dropCandies(function () {
refillBoard(function () {
processMatches();
});
});
});
movesLeft--;
if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
}
if (!specialActivated) {
var matches = findMatches();
if (matches.length > 0) {
processMatches();
movesLeft--;
if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
} else {
// No match, swap back
LK.getSound('fail').play();
var origX1 = c1.col * CELL_SIZE + CELL_SIZE / 2;
var origY1 = c1.row * CELL_SIZE + CELL_SIZE / 2;
var origX2 = c2.col * CELL_SIZE + CELL_SIZE / 2;
var origY2 = c2.row * CELL_SIZE + CELL_SIZE / 2;
tween(c1, {
x: origX1 + 20
}, {
duration: 60,
onFinish: function onFinish() {
tween(c1, {
x: origX1
}, {
duration: 60
});
}
});
tween(c2, {
x: origX2 - 20
}, {
duration: 60,
onFinish: function onFinish() {
tween(c2, {
x: origX2
}, {
duration: 60
});
}
});
// Actually swap back the candies to their original positions in the board and update their row/col
swapCandies(c1, c2, function () {
swapping = false;
deselectAll();
// Re-select the original candy for user feedback
selectedCandy = c1;
c1.setSelected(true);
});
movesLeft--;
if (movesLeft < 0) movesLeft = 0;
updateGUI();
}
}
});
}
};
// Drag end handler
game.up = function (x, y, obj) {
dragActive = false;
dragStartCandy = null;
dragCurrentCandy = null;
// Deselect all if not swapping
if (!swapping && !animating && !isProcessing) {
deselectAll();
}
};
// Game update
game.update = function () {
// No per-frame logic needed for MVP
};
// Start music
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 0.7,
duration: 1000
}
});
// Level label at bottom left
var levelTxt = new Text2('Level: 1', {
size: 70,
fill: "#fff"
});
levelTxt.anchor.set(0, 1);
LK.gui.bottomLeft.addChild(levelTxt);
levelTxt.x = 0;
levelTxt.y = 0;
levelTxt.visible = false;
// Add previous level button at the bottom left (but not in the top left 100x100 area)
var prevLevelBtn = new Text2('Prev', {
size: 90,
fill: "#fff"
});
prevLevelBtn.anchor.set(0, 1);
LK.gui.bottomLeft.addChild(prevLevelBtn);
prevLevelBtn.x = 120; // avoid top left menu area
prevLevelBtn.y = 0;
prevLevelBtn.visible = false;
prevLevelBtn.interactive = true;
prevLevelBtn.buttonMode = true;
prevLevelBtn.down = function (x, y, obj) {
if (currentLevel > 0) {
currentLevel--;
score = 0;
movesLeft = levels[currentLevel].moves;
targetScore = levels[currentLevel].target;
// Save progress
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
updateGUI();
initBoard();
updateCounterPanel();
isProcessing = false;
// Defensive: ensure isProcessing is false after prevLevelBtn
isProcessing = false;
}
};
// Add reset button at the bottom center
var resetBtn = new Text2('Reset', {
size: 90,
fill: "#fff"
});
resetBtn.anchor.set(0.5, 1);
LK.gui.bottom.addChild(resetBtn);
resetBtn.y = 0; // flush to bottom
resetBtn.visible = false;
resetBtn.interactive = true;
resetBtn.buttonMode = true;
resetBtn.down = function (x, y, obj) {
// Reset current level: reset score, moves, board
score = 0;
movesLeft = levels[currentLevel].moves;
targetScore = levels[currentLevel].target;
// Save progress
storage.currentLevel = currentLevel;
storage.score = score;
storage.movesLeft = movesLeft;
updateGUI();
initBoard();
updateCounterPanel();
isProcessing = false;
};
// --- Counter Panel Below Board ---
// This panel will be centered horizontally, just below the board, and always visible
var counterPanel = new Container();
game.addChild(counterPanel);
// Position: center horizontally, just below the board
counterPanel.x = BOARD_OFFSET_X + BOARD_COLS * CELL_SIZE / 2;
counterPanel.y = BOARD_OFFSET_Y + BOARD_ROWS * CELL_SIZE + 40; // 40px below the board
counterPanel.visible = true;
// Counter text (will be updated in updateCounterPanel)
var counterTxt = new Text2('', {
size: 80,
fill: "#fff"
});
counterTxt.anchor.set(0.5, 0);
counterPanel.addChild(counterTxt);
// Helper: update counter panel with current progress
function updateCounterPanel() {
// Compose a summary of all objectives, each with its asset icon and progress
// Remove old icons (keep counterTxt as first child)
while (counterPanel.children.length > 1) {
var ch = counterPanel.children.pop();
ch.destroy && ch.destroy();
}
// For each objective, show icon and progress
var summaryArr = [];
var iconX = -((currentObjectives.length - 1) * 120) / 2; // space icons evenly
for (var i = 0; i < currentObjectives.length; ++i) {
var obj = currentObjectives[i];
var iconId = null;
if (OBJECTIVE_ICONS[obj.mech]) {
iconId = OBJECTIVE_ICONS[obj.mech];
} else if (OBJECTIVE_ICONS.fallback[obj.mech]) {
iconId = OBJECTIVE_ICONS.fallback[obj.mech];
} else {
iconId = OBJECTIVE_ICONS.fallback.valve;
}
var icon = LK.getAsset(iconId, {
anchorX: 0.5,
anchorY: 0.5,
x: iconX + i * 120,
y: 60,
width: 80,
height: 80
});
counterPanel.addChild(icon);
// Progress text below icon
var txt = new Text2(currentObjectiveProgress[i] + " / " + obj.count, {
size: 48,
fill: "#fff"
});
txt.anchor.set(0.5, 0);
txt.x = icon.x;
txt.y = icon.y + 60;
counterPanel.addChild(txt);
}
// Optionally, show a summary at the top (e.g. "Objectives")
counterTxt.setText("Objectives");
}
// Call updateCounterPanel whenever board is initialized or progress changes
// Start game
initBoard();
updateGUI();
updateCounterPanel();
// If the start screen is hidden (game started), show UI
if (typeof startScreen !== "undefined" && !startScreen.visible) {
if (scoreTxt) scoreTxt.visible = true;
if (movesTxt) movesTxt.visible = true;
if (targetTxt) targetTxt.visible = true;
if (levelTopRightTxt) levelTopRightTxt.visible = true;
if (bonusPanel) bonusPanel.visible = true;
if (boardContainer) boardContainer.visible = true;
if (levelTxt) levelTxt.visible = true;
if (prevLevelBtn) prevLevelBtn.visible = true;
if (resetBtn) resetBtn.visible = true;
// Removed reference to nextLevelBtn which is not defined
if (window.levelDescTxt) window.levelDescTxt.visible = true;
if (counterPanel) counterPanel.visible = true;
} else {
// On opening, always hide UI until play is pressed
if (scoreTxt) scoreTxt.visible = false;
if (movesTxt) movesTxt.visible = false;
if (targetTxt) targetTxt.visible = false;
if (levelTopRightTxt) levelTopRightTxt.visible = false;
if (bonusPanel) bonusPanel.visible = false;
if (boardContainer) boardContainer.visible = false;
if (levelTxt) levelTxt.visible = false;
if (prevLevelBtn) prevLevelBtn.visible = false;
if (resetBtn) resetBtn.visible = false;
if (window.levelDescTxt) window.levelDescTxt.visible = false;
if (counterPanel) counterPanel.visible = false;
}