/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Candy class: represents a single candy on the board var Candy = Container.expand(function () { var self = Container.call(this); // Properties self.type = 0; // 0-6, for 7 types self.row = 0; self.col = 0; self.isMatched = false; // Attach asset for candy, anchor center self.candyAsset = null; // Set candy type and update asset self.setType = function (type) { self.type = type; // Remove previous asset if exists if (self.candyAsset) { self.removeChild(self.candyAsset); } // Assign each candy type to a unique asset for visual distinction var assetIds = ['Sg1', // type 0 'Sg2', // type 1 'Sg3', // type 2 'Sg4', // type 3 'Sg5', // type 4 'Sg6', // type 5 'Sg7' // type 6 ]; self.candyAsset = self.attachAsset(assetIds[type], { anchorX: 0.5, anchorY: 0.5 }); }; // Animate pop (match) self.pop = function (_onFinish) { tween(self, { scaleX: 1.3, scaleY: 1.3, alpha: 0 }, { duration: 250, easing: tween.easeIn, onFinish: function onFinish() { self.alpha = 1; self.scaleX = 1; self.scaleY = 1; if (_onFinish) _onFinish(); } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222244 }); /**** * Game Code ****/ // --- Game Constants --- var boardCols = 7; var boardRows = 9; var candyTypes = 7; var candySize = 200; // px, will be scaled to fit var boardPadding = 0; // No padding between candies var moveLimit = 20; var targetScore = 1800; // --- Game State --- var board = []; // 2D array [row][col] of Candy var candies = []; // Flat array of all Candy objects var selectedCandy = null; var swappingCandy = null; var isSwapping = false; var isAnimating = false; var movesLeft = moveLimit; var score = 0; var level = storage.level || 1; // --- UI Elements --- var scoreTxt = new Text2('0', { size: 90, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); var movesTxt = new Text2('Moves: ' + movesLeft, { size: 70, fill: "#fff" }); movesTxt.anchor.set(0.5, 0); LK.gui.top.addChild(movesTxt); movesTxt.y = 110; // Best score UI var bestScore = storage.bestScore || 0; var bestScoreTxt = new Text2('Best: ' + bestScore, { size: 60, fill: "#fff" }); bestScoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(bestScoreTxt); bestScoreTxt.y = 190; // Current score UI (label) var currentScoreTxt = new Text2('Score', { size: 60, fill: "#fff" }); currentScoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(currentScoreTxt); currentScoreTxt.y = 40; // --- Board Positioning --- // Add Sg_bg asset as a background layer, scaled to cover the game area var sgBgAsset = LK.getAsset('Sg_bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 2732 }); game.addChild(sgBgAsset); // Add Bg asset as background, scaled to cover the game area var bgAsset = LK.getAsset('Bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: 2048, height: 2732 }); game.addChild(bgAsset); var boardWidth = boardCols * candySize; var boardHeight = boardRows * candySize; var boardOffsetX = Math.floor((2048 - boardWidth) / 2); var boardOffsetY = Math.floor((2732 - boardHeight) / 2) + 60; // --- Helper Functions --- // Get board position (x, y) for a given row, col function getBoardPos(row, col) { return { x: boardOffsetX + col * candySize + candySize / 2, y: boardOffsetY + row * candySize + candySize / 2 }; } // Get candy at (row, col) function getCandy(row, col) { if (row < 0 || row >= boardRows || col < 0 || col >= boardCols) return null; return board[row][col]; } // Swap two candies in board and update their row/col function swapCandies(c1, c2) { if (!c1 || !c2) { // Defensive: do nothing if either candy is null or undefined return; } var r1 = c1.row, c1c = c1.col, r2 = c2.row, c2c = c2.col; board[r1][c1c] = c2; board[r2][c2c] = c1; c1.row = r2; c1.col = c2c; c2.row = r1; c2.col = c1c; } // Animate swap between two candies function animateSwap(c1, c2, _onFinish2) { if (!c1 || !c2) { // Defensive: do nothing if either candy is null or undefined isAnimating = false; if (_onFinish2) _onFinish2(); return; } isAnimating = true; var pos1 = getBoardPos(c1.row, c1.col); var pos2 = getBoardPos(c2.row, c2.col); tween(c1, { x: pos2.x, y: pos2.y }, { duration: 180, easing: tween.cubicInOut }); tween(c2, { x: pos1.x, y: pos1.y }, { duration: 180, easing: tween.cubicInOut, onFinish: function onFinish() { isAnimating = false; if (_onFinish2) _onFinish2(); } }); } // Animate candy falling to new position function animateMoveTo(candy, newRow, newCol, onFinish) { var pos = getBoardPos(newRow, newCol); tween(candy, { x: pos.x, y: pos.y }, { duration: 180, easing: tween.cubicInOut, onFinish: onFinish }); } // Deselect all candies function deselectAll() { if (selectedCandy) { tween(selectedCandy, { scaleX: 1, scaleY: 1 }, { duration: 100 }); selectedCandy = null; } } // Check if two candies are adjacent function areAdjacent(c1, c2) { return Math.abs(c1.row - c2.row) + Math.abs(c1.col - c2.col) === 1; } // Find all matches on the board, mark candies as isMatched function findMatches() { var found = false; // Clear previous matches for (var r = 0; r < boardRows; r++) { for (var c = 0; c < boardCols; c++) { if (board[r][c]) board[r][c].isMatched = false; } } // Horizontal matches for (var r = 0; r < boardRows; r++) { var matchLen = 1; for (var c = 1; c < boardCols; c++) { if (board[r][c].type === board[r][c - 1].type) { matchLen++; } else { if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[r][c - 1 - k].isMatched = true; } found = true; } matchLen = 1; } } if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[r][boardCols - 1 - k].isMatched = true; } found = true; } } // Vertical matches for (var c = 0; c < boardCols; c++) { var matchLen = 1; for (var r = 1; r < boardRows; r++) { if (board[r][c].type === board[r - 1][c].type) { matchLen++; } else { if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[r - 1 - k][c].isMatched = true; } found = true; } matchLen = 1; } } if (matchLen >= 3) { for (var k = 0; k < matchLen; k++) { board[boardRows - 1 - k][c].isMatched = true; } found = true; } } return found; } // Remove matched candies, return number removed function removeMatches(onFinish) { var removed = 0; var toPop = []; for (var r = 0; r < boardRows; r++) { for (var c = 0; c < boardCols; c++) { var candy = board[r][c]; if (candy.isMatched) { toPop.push(candy); removed++; } } } if (removed === 0) { if (onFinish) onFinish(); return; } var popped = 0; for (var i = 0; i < toPop.length; i++) { // Play sg_crash sound for each exploding candy LK.getSound('Sg_crash').play(); toPop[i].pop(function () { popped++; if (popped === toPop.length) { for (var j = 0; j < toPop.length; j++) { var c = toPop[j]; game.removeChild(c); candies.splice(candies.indexOf(c), 1); board[c.row][c.col] = null; } if (onFinish) onFinish(removed); } }); } } // Drop candies down to fill empty spaces, return true if any moved function dropCandies(onFinish) { var moved = false; var moves = []; for (var c = 0; c < boardCols; c++) { for (var r = boardRows - 1; r >= 0; r--) { if (board[r][c] === null) { // Find first non-null above for (var k = r - 1; k >= 0; k--) { if (board[k][c]) { var candy = board[k][c]; board[r][c] = candy; board[k][c] = null; var oldRow = candy.row; candy.row = r; moves.push({ candy: candy, from: oldRow, to: r, col: c }); break; } } } } } if (moves.length === 0) { if (onFinish) onFinish(false); return; } var finished = 0; for (var i = 0; i < moves.length; i++) { var move = moves[i]; animateMoveTo(move.candy, move.to, move.col, function () { finished++; if (finished === moves.length) { if (onFinish) onFinish(true); } }); } } // Fill empty spaces at the top with new candies function fillBoard(onFinish) { var filled = 0; var toFill = []; for (var c = 0; c < boardCols; c++) { for (var r = 0; r < boardRows; r++) { if (board[r][c] === null) { var candy = new Candy(); var type = Math.floor(Math.random() * candyTypes); candy.setType(type); candy.row = r; candy.col = c; var pos = getBoardPos(r, c); candy.x = pos.x; candy.y = pos.y - 400; // Drop from above game.addChild(candy); candies.push(candy); board[r][c] = candy; toFill.push(candy); } } } if (toFill.length === 0) { if (onFinish) onFinish(); return; } var finished = 0; for (var i = 0; i < toFill.length; i++) { var candy = toFill[i]; var pos = getBoardPos(candy.row, candy.col); animateMoveTo(candy, candy.row, candy.col, function () { finished++; if (finished === toFill.length) { if (onFinish) onFinish(); } }); } } // Check if the board has any possible moves (for future: shuffle if not) function hasPossibleMoves() { // For MVP, skip shuffle, always assume possible moves return true; } // Update UI function updateUI() { scoreTxt.setText(score); movesTxt.setText('Moves: ' + movesLeft); if (score > bestScore) { bestScore = score; storage.bestScore = bestScore; } bestScoreTxt.setText('Best: ' + bestScore); } // Handle end of turn: check for matches, drop, fill, repeat as needed function resolveBoard(afterResolve) { if (findMatches()) { removeMatches(function (removed) { // Award more points for more candies exploded at once: 3=100, 4=150, 5=200, 6+=4x points var points = 0; if (removed >= 3) { if (removed === 3) { points = 100; } else if (removed === 4) { points = Math.floor(100 * 1.5); // 150 } else if (removed === 5) { points = 200; } else if (removed >= 6) { // 6-pack or more: 4x the base points for that many candies // Base: 3=100, 4=150, 5=200, 6=250, 7=300, 8=350, etc. var base = 100 + Math.max(0, removed - 3) * 50; points = base * 4; } } score += points; updateUI(); dropCandies(function () { fillBoard(function () { resolveBoard(afterResolve); }); }); }); } else { if (afterResolve) afterResolve(); } } // Handle win/lose function checkEnd() { if (movesLeft <= 0) { // Only end the game if there are no matches left to resolve if (!findMatches()) { // Show game over when moves are finished and board is resolved LK.showGameOver(); return true; } // Otherwise, let the board resolve naturally before ending return false; } return false; } // --- Board Initialization --- function createBoard() { // Clear previous for (var i = 0; i < candies.length; i++) { game.removeChild(candies[i]); } candies = []; board = []; for (var r = 0; r < boardRows; r++) { board[r] = []; for (var c = 0; c < boardCols; c++) { var candy = new Candy(); var type = Math.floor(Math.random() * candyTypes); candy.setType(type); candy.row = r; candy.col = c; var pos = getBoardPos(r, c); candy.x = pos.x; candy.y = pos.y; game.addChild(candy); candies.push(candy); board[r][c] = candy; } } // Remove any initial matches while (findMatches()) { for (var r = 0; r < boardRows; r++) { for (var c = 0; c < boardCols; c++) { if (board[r][c] && board[r][c].isMatched) { var newType; do { newType = Math.floor(Math.random() * candyTypes); } while (r > 0 && board[r - 1][c] && board[r - 1][c].type === newType || c > 0 && board[r][c - 1] && board[r][c - 1].type === newType); board[r][c].setType(newType); board[r][c].isMatched = false; } } } } } // --- Input Handling --- // Find candy at (x, y) in game coordinates function findCandyAt(x, y) { for (var i = 0; i < candies.length; i++) { var c = candies[i]; var dx = x - c.x; var dy = y - c.y; if (Math.abs(dx) < candySize / 2 && Math.abs(dy) < candySize / 2) { return c; } } return null; } // Handle tap or drag game.down = function (x, y, obj) { if (isAnimating) return; var c = findCandyAt(x, y); if (!c) { deselectAll(); return; } if (!selectedCandy) { selectedCandy = c; tween(selectedCandy, { scaleX: 1.15, scaleY: 1.15 }, { duration: 100 }); } else if (selectedCandy === c) { deselectAll(); } else if (areAdjacent(selectedCandy, c)) { // Swap attempt isSwapping = true; swappingCandy = c; // Animate swap animateSwap(selectedCandy, swappingCandy, function () { swapCandies(selectedCandy, swappingCandy); // Check if swap creates a match if (findMatches()) { movesLeft--; updateUI(); resolveBoard(function () { deselectAll(); isSwapping = false; swappingCandy = null; checkEnd(); }); } else { // No match, swap back and decrement moves (wrong move) // Play Wrong sound LK.getSound('Wrong').play(); movesLeft--; updateUI(); animateSwap(selectedCandy, swappingCandy, function () { swapCandies(selectedCandy, swappingCandy); deselectAll(); isSwapping = false; swappingCandy = null; checkEnd(); }); } }); } else { // Select new candy tween(selectedCandy, { scaleX: 1, scaleY: 1 }, { duration: 100 }); selectedCandy = c; tween(selectedCandy, { scaleX: 1.15, scaleY: 1.15 }, { duration: 100 }); } }; // Drag to swap game.move = function (x, y, obj) { if (isAnimating || !selectedCandy) return; var c = findCandyAt(x, y); if (c && c !== selectedCandy && areAdjacent(selectedCandy, c)) { game.down(x, y, obj); } }; game.up = function (x, y, obj) { // No-op for now }; // --- Game Start --- function startGame() { level = storage.level || 1; score = 0; if (level === 2) { movesLeft = 10; targetScore = 2500; } else { movesLeft = moveLimit; targetScore = 1800; } updateUI(); createBoard(); deselectAll(); isAnimating = false; isSwapping = false; swappingCandy = null; selectedCandy = null; // Start Fnt music LK.playMusic('Fnt'); // Remove any matches at start resolveBoard(); } startGame(); // --- Game Tick --- game.update = function () { // No per-frame logic needed for MVP };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Candy class: represents a single candy on the board
var Candy = Container.expand(function () {
var self = Container.call(this);
// Properties
self.type = 0; // 0-6, for 7 types
self.row = 0;
self.col = 0;
self.isMatched = false;
// Attach asset for candy, anchor center
self.candyAsset = null;
// Set candy type and update asset
self.setType = function (type) {
self.type = type;
// Remove previous asset if exists
if (self.candyAsset) {
self.removeChild(self.candyAsset);
}
// Assign each candy type to a unique asset for visual distinction
var assetIds = ['Sg1',
// type 0
'Sg2',
// type 1
'Sg3',
// type 2
'Sg4',
// type 3
'Sg5',
// type 4
'Sg6',
// type 5
'Sg7' // type 6
];
self.candyAsset = self.attachAsset(assetIds[type], {
anchorX: 0.5,
anchorY: 0.5
});
};
// Animate pop (match)
self.pop = function (_onFinish) {
tween(self, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0
}, {
duration: 250,
easing: tween.easeIn,
onFinish: function onFinish() {
self.alpha = 1;
self.scaleX = 1;
self.scaleY = 1;
if (_onFinish) _onFinish();
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222244
});
/****
* Game Code
****/
// --- Game Constants ---
var boardCols = 7;
var boardRows = 9;
var candyTypes = 7;
var candySize = 200; // px, will be scaled to fit
var boardPadding = 0; // No padding between candies
var moveLimit = 20;
var targetScore = 1800;
// --- Game State ---
var board = []; // 2D array [row][col] of Candy
var candies = []; // Flat array of all Candy objects
var selectedCandy = null;
var swappingCandy = null;
var isSwapping = false;
var isAnimating = false;
var movesLeft = moveLimit;
var score = 0;
var level = storage.level || 1;
// --- UI Elements ---
var scoreTxt = new Text2('0', {
size: 90,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var movesTxt = new Text2('Moves: ' + movesLeft, {
size: 70,
fill: "#fff"
});
movesTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(movesTxt);
movesTxt.y = 110;
// Best score UI
var bestScore = storage.bestScore || 0;
var bestScoreTxt = new Text2('Best: ' + bestScore, {
size: 60,
fill: "#fff"
});
bestScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(bestScoreTxt);
bestScoreTxt.y = 190;
// Current score UI (label)
var currentScoreTxt = new Text2('Score', {
size: 60,
fill: "#fff"
});
currentScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(currentScoreTxt);
currentScoreTxt.y = 40;
// --- Board Positioning ---
// Add Sg_bg asset as a background layer, scaled to cover the game area
var sgBgAsset = LK.getAsset('Sg_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChild(sgBgAsset);
// Add Bg asset as background, scaled to cover the game area
var bgAsset = LK.getAsset('Bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChild(bgAsset);
var boardWidth = boardCols * candySize;
var boardHeight = boardRows * candySize;
var boardOffsetX = Math.floor((2048 - boardWidth) / 2);
var boardOffsetY = Math.floor((2732 - boardHeight) / 2) + 60;
// --- Helper Functions ---
// Get board position (x, y) for a given row, col
function getBoardPos(row, col) {
return {
x: boardOffsetX + col * candySize + candySize / 2,
y: boardOffsetY + row * candySize + candySize / 2
};
}
// Get candy at (row, col)
function getCandy(row, col) {
if (row < 0 || row >= boardRows || col < 0 || col >= boardCols) return null;
return board[row][col];
}
// Swap two candies in board and update their row/col
function swapCandies(c1, c2) {
if (!c1 || !c2) {
// Defensive: do nothing if either candy is null or undefined
return;
}
var r1 = c1.row,
c1c = c1.col,
r2 = c2.row,
c2c = c2.col;
board[r1][c1c] = c2;
board[r2][c2c] = c1;
c1.row = r2;
c1.col = c2c;
c2.row = r1;
c2.col = c1c;
}
// Animate swap between two candies
function animateSwap(c1, c2, _onFinish2) {
if (!c1 || !c2) {
// Defensive: do nothing if either candy is null or undefined
isAnimating = false;
if (_onFinish2) _onFinish2();
return;
}
isAnimating = true;
var pos1 = getBoardPos(c1.row, c1.col);
var pos2 = getBoardPos(c2.row, c2.col);
tween(c1, {
x: pos2.x,
y: pos2.y
}, {
duration: 180,
easing: tween.cubicInOut
});
tween(c2, {
x: pos1.x,
y: pos1.y
}, {
duration: 180,
easing: tween.cubicInOut,
onFinish: function onFinish() {
isAnimating = false;
if (_onFinish2) _onFinish2();
}
});
}
// Animate candy falling to new position
function animateMoveTo(candy, newRow, newCol, onFinish) {
var pos = getBoardPos(newRow, newCol);
tween(candy, {
x: pos.x,
y: pos.y
}, {
duration: 180,
easing: tween.cubicInOut,
onFinish: onFinish
});
}
// Deselect all candies
function deselectAll() {
if (selectedCandy) {
tween(selectedCandy, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
selectedCandy = null;
}
}
// Check if two candies are adjacent
function areAdjacent(c1, c2) {
return Math.abs(c1.row - c2.row) + Math.abs(c1.col - c2.col) === 1;
}
// Find all matches on the board, mark candies as isMatched
function findMatches() {
var found = false;
// Clear previous matches
for (var r = 0; r < boardRows; r++) {
for (var c = 0; c < boardCols; c++) {
if (board[r][c]) board[r][c].isMatched = false;
}
}
// Horizontal matches
for (var r = 0; r < boardRows; r++) {
var matchLen = 1;
for (var c = 1; c < boardCols; c++) {
if (board[r][c].type === board[r][c - 1].type) {
matchLen++;
} else {
if (matchLen >= 3) {
for (var k = 0; k < matchLen; k++) {
board[r][c - 1 - k].isMatched = true;
}
found = true;
}
matchLen = 1;
}
}
if (matchLen >= 3) {
for (var k = 0; k < matchLen; k++) {
board[r][boardCols - 1 - k].isMatched = true;
}
found = true;
}
}
// Vertical matches
for (var c = 0; c < boardCols; c++) {
var matchLen = 1;
for (var r = 1; r < boardRows; r++) {
if (board[r][c].type === board[r - 1][c].type) {
matchLen++;
} else {
if (matchLen >= 3) {
for (var k = 0; k < matchLen; k++) {
board[r - 1 - k][c].isMatched = true;
}
found = true;
}
matchLen = 1;
}
}
if (matchLen >= 3) {
for (var k = 0; k < matchLen; k++) {
board[boardRows - 1 - k][c].isMatched = true;
}
found = true;
}
}
return found;
}
// Remove matched candies, return number removed
function removeMatches(onFinish) {
var removed = 0;
var toPop = [];
for (var r = 0; r < boardRows; r++) {
for (var c = 0; c < boardCols; c++) {
var candy = board[r][c];
if (candy.isMatched) {
toPop.push(candy);
removed++;
}
}
}
if (removed === 0) {
if (onFinish) onFinish();
return;
}
var popped = 0;
for (var i = 0; i < toPop.length; i++) {
// Play sg_crash sound for each exploding candy
LK.getSound('Sg_crash').play();
toPop[i].pop(function () {
popped++;
if (popped === toPop.length) {
for (var j = 0; j < toPop.length; j++) {
var c = toPop[j];
game.removeChild(c);
candies.splice(candies.indexOf(c), 1);
board[c.row][c.col] = null;
}
if (onFinish) onFinish(removed);
}
});
}
}
// Drop candies down to fill empty spaces, return true if any moved
function dropCandies(onFinish) {
var moved = false;
var moves = [];
for (var c = 0; c < boardCols; c++) {
for (var r = boardRows - 1; r >= 0; r--) {
if (board[r][c] === null) {
// Find first non-null above
for (var k = r - 1; k >= 0; k--) {
if (board[k][c]) {
var candy = board[k][c];
board[r][c] = candy;
board[k][c] = null;
var oldRow = candy.row;
candy.row = r;
moves.push({
candy: candy,
from: oldRow,
to: r,
col: c
});
break;
}
}
}
}
}
if (moves.length === 0) {
if (onFinish) onFinish(false);
return;
}
var finished = 0;
for (var i = 0; i < moves.length; i++) {
var move = moves[i];
animateMoveTo(move.candy, move.to, move.col, function () {
finished++;
if (finished === moves.length) {
if (onFinish) onFinish(true);
}
});
}
}
// Fill empty spaces at the top with new candies
function fillBoard(onFinish) {
var filled = 0;
var toFill = [];
for (var c = 0; c < boardCols; c++) {
for (var r = 0; r < boardRows; r++) {
if (board[r][c] === null) {
var candy = new Candy();
var type = Math.floor(Math.random() * candyTypes);
candy.setType(type);
candy.row = r;
candy.col = c;
var pos = getBoardPos(r, c);
candy.x = pos.x;
candy.y = pos.y - 400; // Drop from above
game.addChild(candy);
candies.push(candy);
board[r][c] = candy;
toFill.push(candy);
}
}
}
if (toFill.length === 0) {
if (onFinish) onFinish();
return;
}
var finished = 0;
for (var i = 0; i < toFill.length; i++) {
var candy = toFill[i];
var pos = getBoardPos(candy.row, candy.col);
animateMoveTo(candy, candy.row, candy.col, function () {
finished++;
if (finished === toFill.length) {
if (onFinish) onFinish();
}
});
}
}
// Check if the board has any possible moves (for future: shuffle if not)
function hasPossibleMoves() {
// For MVP, skip shuffle, always assume possible moves
return true;
}
// Update UI
function updateUI() {
scoreTxt.setText(score);
movesTxt.setText('Moves: ' + movesLeft);
if (score > bestScore) {
bestScore = score;
storage.bestScore = bestScore;
}
bestScoreTxt.setText('Best: ' + bestScore);
}
// Handle end of turn: check for matches, drop, fill, repeat as needed
function resolveBoard(afterResolve) {
if (findMatches()) {
removeMatches(function (removed) {
// Award more points for more candies exploded at once: 3=100, 4=150, 5=200, 6+=4x points
var points = 0;
if (removed >= 3) {
if (removed === 3) {
points = 100;
} else if (removed === 4) {
points = Math.floor(100 * 1.5); // 150
} else if (removed === 5) {
points = 200;
} else if (removed >= 6) {
// 6-pack or more: 4x the base points for that many candies
// Base: 3=100, 4=150, 5=200, 6=250, 7=300, 8=350, etc.
var base = 100 + Math.max(0, removed - 3) * 50;
points = base * 4;
}
}
score += points;
updateUI();
dropCandies(function () {
fillBoard(function () {
resolveBoard(afterResolve);
});
});
});
} else {
if (afterResolve) afterResolve();
}
}
// Handle win/lose
function checkEnd() {
if (movesLeft <= 0) {
// Only end the game if there are no matches left to resolve
if (!findMatches()) {
// Show game over when moves are finished and board is resolved
LK.showGameOver();
return true;
}
// Otherwise, let the board resolve naturally before ending
return false;
}
return false;
}
// --- Board Initialization ---
function createBoard() {
// Clear previous
for (var i = 0; i < candies.length; i++) {
game.removeChild(candies[i]);
}
candies = [];
board = [];
for (var r = 0; r < boardRows; r++) {
board[r] = [];
for (var c = 0; c < boardCols; c++) {
var candy = new Candy();
var type = Math.floor(Math.random() * candyTypes);
candy.setType(type);
candy.row = r;
candy.col = c;
var pos = getBoardPos(r, c);
candy.x = pos.x;
candy.y = pos.y;
game.addChild(candy);
candies.push(candy);
board[r][c] = candy;
}
}
// Remove any initial matches
while (findMatches()) {
for (var r = 0; r < boardRows; r++) {
for (var c = 0; c < boardCols; c++) {
if (board[r][c] && board[r][c].isMatched) {
var newType;
do {
newType = Math.floor(Math.random() * candyTypes);
} while (r > 0 && board[r - 1][c] && board[r - 1][c].type === newType || c > 0 && board[r][c - 1] && board[r][c - 1].type === newType);
board[r][c].setType(newType);
board[r][c].isMatched = false;
}
}
}
}
}
// --- Input Handling ---
// Find candy at (x, y) in game coordinates
function findCandyAt(x, y) {
for (var i = 0; i < candies.length; i++) {
var c = candies[i];
var dx = x - c.x;
var dy = y - c.y;
if (Math.abs(dx) < candySize / 2 && Math.abs(dy) < candySize / 2) {
return c;
}
}
return null;
}
// Handle tap or drag
game.down = function (x, y, obj) {
if (isAnimating) return;
var c = findCandyAt(x, y);
if (!c) {
deselectAll();
return;
}
if (!selectedCandy) {
selectedCandy = c;
tween(selectedCandy, {
scaleX: 1.15,
scaleY: 1.15
}, {
duration: 100
});
} else if (selectedCandy === c) {
deselectAll();
} else if (areAdjacent(selectedCandy, c)) {
// Swap attempt
isSwapping = true;
swappingCandy = c;
// Animate swap
animateSwap(selectedCandy, swappingCandy, function () {
swapCandies(selectedCandy, swappingCandy);
// Check if swap creates a match
if (findMatches()) {
movesLeft--;
updateUI();
resolveBoard(function () {
deselectAll();
isSwapping = false;
swappingCandy = null;
checkEnd();
});
} else {
// No match, swap back and decrement moves (wrong move)
// Play Wrong sound
LK.getSound('Wrong').play();
movesLeft--;
updateUI();
animateSwap(selectedCandy, swappingCandy, function () {
swapCandies(selectedCandy, swappingCandy);
deselectAll();
isSwapping = false;
swappingCandy = null;
checkEnd();
});
}
});
} else {
// Select new candy
tween(selectedCandy, {
scaleX: 1,
scaleY: 1
}, {
duration: 100
});
selectedCandy = c;
tween(selectedCandy, {
scaleX: 1.15,
scaleY: 1.15
}, {
duration: 100
});
}
};
// Drag to swap
game.move = function (x, y, obj) {
if (isAnimating || !selectedCandy) return;
var c = findCandyAt(x, y);
if (c && c !== selectedCandy && areAdjacent(selectedCandy, c)) {
game.down(x, y, obj);
}
};
game.up = function (x, y, obj) {
// No-op for now
};
// --- Game Start ---
function startGame() {
level = storage.level || 1;
score = 0;
if (level === 2) {
movesLeft = 10;
targetScore = 2500;
} else {
movesLeft = moveLimit;
targetScore = 1800;
}
updateUI();
createBoard();
deselectAll();
isAnimating = false;
isSwapping = false;
swappingCandy = null;
selectedCandy = null;
// Start Fnt music
LK.playMusic('Fnt');
// Remove any matches at start
resolveBoard();
}
startGame();
// --- Game Tick ---
game.update = function () {
// No per-frame logic needed for MVP
};