/****
* 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;
// Add "env games" text in the bottom right corner
var envGamesTxt = new Text2('env games', {
size: 40,
fill: "#fff"
});
envGamesTxt.anchor.set(1, 1);
LK.gui.bottomRight.addChild(envGamesTxt);
// --- 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;
// Add "env games" text in the bottom right corner
var envGamesTxt = new Text2('env games', {
size: 40,
fill: "#fff"
});
envGamesTxt.anchor.set(1, 1);
LK.gui.bottomRight.addChild(envGamesTxt);
// --- 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
};