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
****/
// Board data
// Candy shapes/colors
// Special candies
// Blockers
// Sounds
// Music
// Candy types
// Candy and blocker assets sized to fit CELL_SIZE (200x200) for main board
// Prism button asset sized for large UI button (500x160 in use, so use 500x160 for best quality)
// Candy assets
// Blocker assets
// UI assets
// Music and sounds
//{2.1}
// blue valve
// orange pump
// red gauge
// green elbow
// gray locked pipe
// purple T-junction
// Tracker icons (64x64) for each mechanical part
// Fallback shapes for missing tracker icons (V/P/G/E/L/T)
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
if (objectiveAssetNames.length > 0 && Math.random() < 0.5) {
// 50% 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 higher probability
if (objectiveAssetNames.length > 0 && Math.random() < 0.5) {
// --- 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;
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();
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);
// Show next level screen after passing the chapter
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();
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;
} 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;
} ===================================================================
--- original.js
+++ change.js
@@ -2671,5 +2671,18 @@
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;
}
\ No newline at end of file