/****
* 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;
}