User prompt
fix all failed problems
User prompt
fix bugs game sometimes freeze
User prompt
adjust the random rate of new boxes coming from above to reduce the probability of them exploding on their own
User prompt
fix bugs for opening
User prompt
try again
User prompt
fix bugs
User prompt
set random proportions of inboxes
User prompt
DON'T CREATE TOO MANY ASSETS, REDUCE THE NUMBER OF NEW ASSETS CREATED FROM EXISTING ONES, CONNECT NEW ASSETS TO MISSIONS IN DIFFERENT QUANTITIES
User prompt
WHEN A LEVEL IS GAINED, MOVE TO THE NEXT LEVEL 100 CHAPTERS ARE DIFFERENT FROM EACH OTHER WITH INCREASING DIFFICULTY AND CHANGING MISSIONS. NEXT LEVEL SCREEN AFTER PASSING THE CHAPTER
User prompt
IF WE ARE GOING TO DO THE ASSETS MISSION IN THE GAME, LET IT CONTINUE TO SPAWN RANDOMLY WITHIN THAT SECTION
User prompt
MISSIONS AND ASSETS CREATED IN THE GAME DO NOT HOLD EACH OTHER, MAP THEM. IF YOU WANT AN ASSET TO DISAPPEAR IN A CERTAIN NUMBER OF TIMES IN A CHAPTER, HAVE THAT ASSET IN THE GAME
User prompt
RECALCULATE AND REARRANGE THE NUMBER OF MOVES THE NUMBER OF MOVES CAN NEVER BE NEGATIVE. EACH TIME A LEVEL IS GAME OVER, INCREASE THE NUMBER OF MOVES BACK TO THE NUMBER REQUIRED FOR THE INITIAL LEVEL
User prompt
EDIT THE NUMBER OF MOVES EDIT ALL ACCORDING TO DIFFICULTY AND LEVEL
User prompt
RESTART THE CHAPTER BY RESTARTING EVERY TIME THE GAME OVER
User prompt
Ensure complete consistency between level objectives and game assets in the mechanical match-3 game. Rebuild the objective system to exclusively use existing mechanical-themed assets: valves (blue), pumps (orange), gauges (red), elbows (green), locked pipes (gray), and T-junctions (purple). Replace all candy-themed terminology with mechanical terms - 'destroy 15 valves' instead of 'candies'. Add missing assets for objective trackers: create 64x64px icons for each mechanical part using the same color scheme. Rigorously link objectives to asset names in code: when an objective says 'clear 20 locked pipes', only tile_locked assets should increment the counter. Implement real-time objective validation that triggers error logs if asset-objective mismatches occur. Add fallback shapes with type initials (V/P/G/E/L/T) for any missing tracker icons
User prompt
THE COUNTER IS IMMEDIATELY BELOW, IN THE CENTER AT THE END OF THE MATRIX AND VISIBLE
User prompt
REMOVE BLUE GEAR CODES AND RULES AND ASSETS FROM GAME
User prompt
CHANGE ALL LEVEL MİSSİONS AND FİX ALL OF THEM ASSETS AND CODES
User prompt
YOU SAY COLLECT 10 BLUE GEARS. YOU DON'T COUNT WHEN THE GEARS EXPLODE IN THE EPISODE. THERE IS A BUG. MATCH THE GIVEN TASKS WITH THE ASSETS. CODE AND ASSETS AND RULES SHOULD BE CONNECTED WITH EACH OTHER
User prompt
WRITE DOWN THE PROCESSES SUCH AS THE NUMBER OF TASKS PERFORMED AND HOW MANY ARE LEFT BELOW. ANOTHER IMPORTANT POINT IS TO LINK ALL TASKS TO ASSETS
User prompt
OTHER BOXES ARE VISIBLE BEHIND SOME BOXES. SOLVE THE BUG
User prompt
ADD BLUE GEARS İN GAME AND ASSETS,
User prompt
ADD THEİR ASSETS İN GAME
/****
* 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)
var levelObjectives = [
// Level 1: Collect 15 valves (blue)
[{
type: OBJECTIVE_COLLECT,
mech: 'valve',
asset: 'valve_blue',
count: 15
}],
// Level 2: Collect 12 pumps (orange)
[{
type: OBJECTIVE_COLLECT,
mech: 'pump',
asset: 'pump_orange',
count: 12
}],
// Level 3: Collect 10 gauges (red) and 10 elbows (green)
[{
type: OBJECTIVE_COLLECT,
mech: 'gauge',
asset: 'gauge_red',
count: 10
}, {
type: OBJECTIVE_COLLECT,
mech: 'elbow',
asset: 'elbow_green',
count: 10
}],
// Level 4: Clear 8 locked pipes (gray), collect 8 T-junctions (purple)
[{
type: OBJECTIVE_CLEAR,
mech: 'locked',
asset: 'tile_locked',
count: 8
}, {
type: OBJECTIVE_COLLECT,
mech: 'tjunction',
asset: 'tjunction_purple',
count: 8
}],
// Level 5: Collect 10 valves, 10 pumps, 10 gauges
[{
type: OBJECTIVE_COLLECT,
mech: 'valve',
asset: 'valve_blue',
count: 10
}, {
type: OBJECTIVE_COLLECT,
mech: 'pump',
asset: 'pump_orange',
count: 10
}, {
type: OBJECTIVE_COLLECT,
mech: 'gauge',
asset: 'gauge_red',
count: 10
}],
// Level 6: Clear 12 locked pipes, collect 8 elbows
[{
type: OBJECTIVE_CLEAR,
mech: 'locked',
asset: 'tile_locked',
count: 12
}, {
type: OBJECTIVE_COLLECT,
mech: 'elbow',
asset: 'elbow_green',
count: 8
}],
// Level 7: Collect 8 T-junctions, clear 8 locked pipes
[{
type: OBJECTIVE_COLLECT,
mech: 'tjunction',
asset: 'tjunction_purple',
count: 8
}, {
type: OBJECTIVE_CLEAR,
mech: 'locked',
asset: 'tile_locked',
count: 8
}],
// Level 8: Collect 10 of each part (valve, pump, gauge, elbow, t-junction)
[{
type: OBJECTIVE_COLLECT,
mech: 'valve',
asset: 'valve_blue',
count: 10
}, {
type: OBJECTIVE_COLLECT,
mech: 'pump',
asset: 'pump_orange',
count: 10
}, {
type: OBJECTIVE_COLLECT,
mech: 'gauge',
asset: 'gauge_red',
count: 10
}, {
type: OBJECTIVE_COLLECT,
mech: 'elbow',
asset: 'elbow_green',
count: 10
}, {
type: OBJECTIVE_COLLECT,
mech: 'tjunction',
asset: 'tjunction_purple',
count: 10
}],
// Level 9: Clear 15 locked pipes, collect 5 of each part
[{
type: OBJECTIVE_CLEAR,
mech: 'locked',
asset: 'tile_locked',
count: 15
}, {
type: OBJECTIVE_COLLECT,
mech: 'valve',
asset: 'valve_blue',
count: 5
}, {
type: OBJECTIVE_COLLECT,
mech: 'pump',
asset: 'pump_orange',
count: 5
}, {
type: OBJECTIVE_COLLECT,
mech: 'gauge',
asset: 'gauge_red',
count: 5
}, {
type: OBJECTIVE_COLLECT,
mech: 'elbow',
asset: 'elbow_green',
count: 5
}, {
type: OBJECTIVE_COLLECT,
mech: 'tjunction',
asset: 'tjunction_purple',
count: 5
}]
// ...repeat or randomize for higher levels
];
// 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 mechanical icon mapping, fallback to shape if missing
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) iconId = OBJECTIVE_ICONS[obj.color];
if (obj.type === OBJECTIVE_CLEAR) iconId = obj.blocker === BLOCKER_CHOCOLATE ? OBJECTIVE_ICONS.chocolate : OBJECTIVE_ICONS.ice;
if (obj.type === OBJECTIVE_RESCUE) iconId = OBJECTIVE_ICONS.mechanic;
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+
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;
}
}
}
}
// 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;
// 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;
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 (locked pipes)
var cleared = 0;
for (var j = 0; j < matches.length; ++j) {
if (matches[j].blocker === BLOCKER_CHOCOLATE && obj.asset === 'tile_locked' && matches[j].isMatched) {
cleared++;
} else if (matches[j].blocker === BLOCKER_ICE && obj.asset === 'tile_locked' && matches[j].isMatched) {
// If you want to count ice as locked, add here, else skip
} else if (matches[j].isMatched && obj.asset && matches[j].blocker !== BLOCKER_CHOCOLATE && matches[j].blocker !== BLOCKER_ICE) {
// Error log if a match is counted for the wrong asset
console.error("Objective mismatch: tried to count blocker " + matches[j].blocker + " for " + obj.asset);
}
}
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();
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();
}, 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) {
processMatches();
} 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);
LK.showYouWin();
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) {
descLines.push('Collect ' + obj.count + ' ' + obj.color + ' gears');
}
if (obj.type === OBJECTIVE_CLEAR) descLines.push('Clear ' + obj.count + ' ' + (obj.blocker === BLOCKER_CHOCOLATE ? 'locked pipes' : 'ice'));
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();
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();
}, 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();
}
};
// 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();
};
// --- 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;
} ===================================================================
--- original.js
+++ change.js
@@ -1672,8 +1672,10 @@
});
});
});
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();
@@ -1721,8 +1723,9 @@
});
});
});
movesLeft--;
+ if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
}
@@ -1745,8 +1748,9 @@
});
});
});
movesLeft--;
+ if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
}
@@ -1776,8 +1780,9 @@
});
});
});
movesLeft--;
+ if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
}
@@ -1786,8 +1791,9 @@
var matches = findMatches();
if (matches.length > 0) {
processMatches();
movesLeft--;
+ if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
} else {
// No match, swap back
@@ -1829,8 +1835,9 @@
selectedCandy = c1;
c1.setSelected(true);
});
movesLeft--;
+ if (movesLeft < 0) movesLeft = 0;
updateGUI();
}
}
});
@@ -1912,9 +1919,10 @@
LK.showGameOver();
LK.setTimeout(function () {
// Reset current level: reset score, moves, board
score = 0;
- movesLeft = levels[currentLevel].moves;
+ // 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;
@@ -2041,9 +2049,10 @@
LK.showGameOver();
LK.setTimeout(function () {
// Reset current level: reset score, moves, board
score = 0;
- movesLeft = levels[currentLevel].moves;
+ // 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;
@@ -2113,8 +2122,9 @@
});
});
});
movesLeft--; // Using a bonus still costs a move
+ if (movesLeft < 0) movesLeft = 0;
updateGUI();
selectedBonus = null;
bombBtnBg.scaleX = bombBtnBg.scaleY = 1.0;
deselectAll();
@@ -2165,8 +2175,9 @@
});
});
});
movesLeft--;
+ if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
} else if (c1.special === SPECIAL_STRIPED && c2.special === SPECIAL_STRIPED) {
@@ -2187,8 +2198,9 @@
});
});
});
movesLeft--;
+ if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
} else if (c1.special === SPECIAL_BOMB || c2.special === SPECIAL_BOMB) {
@@ -2216,8 +2228,9 @@
});
});
});
movesLeft--;
+ if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
specialActivated = true;
}
@@ -2225,8 +2238,9 @@
var matches = findMatches();
if (matches.length > 0) {
processMatches();
movesLeft--;
+ if (movesLeft < 0) movesLeft = 0;
updateGUI();
swapping = false;
} else {
// No match, swap back
@@ -2267,8 +2281,9 @@
selectedCandy = c1;
c1.setSelected(true);
});
movesLeft--;
+ if (movesLeft < 0) movesLeft = 0;
updateGUI();
}
}
});