/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Block class: represents a block on the grid or in the draggable area
var Block = Container.expand(function () {
var self = Container.call(this);
// Block color index: 0=red, 1=green, 2=blue, 3=yellow, 4=purple, 5=white
self.colorIndex = 0;
self.gridX = -1; // grid position, -1 if not on grid
self.gridY = -1;
self.isOnGrid = false;
// Attach asset based on colorIndex
self.setColor = function (idx) {
self.colorIndex = idx;
if (self.blockAsset) {
self.removeChild(self.blockAsset);
}
if (self.faceAsset) {
self.removeChild(self.faceAsset);
}
var colorNames = ['red', 'green', 'blue', 'yellow', 'purple', 'white'];
var assetId = 'block_' + colorNames[idx];
self.blockAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// --- Add 3D-like shading and highlights ---
// Main highlight (top left)
var highlight = LK.getAsset('block_white', {
width: 60,
height: 60,
anchorX: 0.2,
anchorY: 0.2,
x: -36,
y: -36,
alpha: 0.18
});
self.addChild(highlight);
// Side shadow (bottom right)
var shadow = LK.getAsset('block_black', {
width: 80,
height: 80,
anchorX: 0.8,
anchorY: 0.8,
x: 36,
y: 36,
alpha: 0.10
});
self.addChild(shadow);
// Edge highlight (top edge)
var edgeHighlight = LK.getAsset('block_white', {
width: 80,
height: 16,
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: -44,
alpha: 0.10
});
self.addChild(edgeHighlight);
// Edge shadow (bottom edge)
var edgeShadow = LK.getAsset('block_black', {
width: 80,
height: 16,
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 44,
alpha: 0.08
});
self.addChild(edgeShadow);
// Add a cute manga face overlay (big manga eyes, highlights, and a bigger smile)
self.faceAsset = new Container();
// Manga left eye (big white)
var eyeL = LK.getAsset('block_white', {
width: 36,
height: 36,
anchorX: 0.5,
anchorY: 0.5,
x: -36,
y: -22,
alpha: 0.98
});
// Manga right eye (big white)
var eyeR = LK.getAsset('block_white', {
width: 36,
height: 36,
anchorX: 0.5,
anchorY: 0.5,
x: 36,
y: -22,
alpha: 0.98
});
// Left eye pupil (black)
var pupilL = LK.getAsset('block_black', {
width: 16,
height: 16,
anchorX: 0.5,
anchorY: 0.5,
x: -36,
y: -22,
alpha: 0.95
});
// Right eye pupil (black)
var pupilR = LK.getAsset('block_black', {
width: 16,
height: 16,
anchorX: 0.5,
anchorY: 0.5,
x: 36,
y: -22,
alpha: 0.95
});
// Left eye highlight (white)
var eyeHL = LK.getAsset('block_white', {
width: 7,
height: 7,
anchorX: 0.5,
anchorY: 0.5,
x: -40,
y: -28,
alpha: 0.85
});
// Right eye highlight (white)
var eyeHR = LK.getAsset('block_white', {
width: 7,
height: 7,
anchorX: 0.5,
anchorY: 0.5,
x: 32,
y: -28,
alpha: 0.85
});
// Cheeks
var cheekL = LK.getAsset('block_red', {
width: 18,
height: 10,
anchorX: 0.5,
anchorY: 0.5,
x: -38,
y: 18,
alpha: 0.18
});
var cheekR = LK.getAsset('block_red', {
width: 18,
height: 10,
anchorX: 0.5,
anchorY: 0.5,
x: 38,
y: 18,
alpha: 0.18
});
// Big smile
var smileAssetId = typeof LK.assets !== "undefined" && LK.assets && typeof LK.assets['block_black'] !== "undefined" ? 'block_black' : 'block_blue';
var smile = LK.getAsset(smileAssetId, {
width: 60,
height: 16,
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 22,
alpha: 0.8,
rotation: 0.08
});
// Smile highlight (white, for manga shine)
var smileHL = LK.getAsset('block_white', {
width: 18,
height: 4,
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 18,
alpha: 0.18,
rotation: 0.08
});
self.faceAsset.addChild(eyeL);
self.faceAsset.addChild(eyeR);
self.faceAsset.addChild(pupilL);
self.faceAsset.addChild(pupilR);
self.faceAsset.addChild(eyeHL);
self.faceAsset.addChild(eyeHR);
self.faceAsset.addChild(cheekL);
self.faceAsset.addChild(cheekR);
self.faceAsset.addChild(smile);
self.faceAsset.addChild(smileHL);
self.addChild(self.faceAsset);
};
self.setColor(0);
// Animate merge
self.animateMerge = function (_onFinish) {
tween(self, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn,
onFinish: _onFinish
});
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222233
});
/****
* Game Code
****/
// Sound for merge
// Merged block (final color)
// We'll use 5 colors: red, green, blue, yellow, purple
// Define block shapes for each color and merged color
// --- Game Constants ---
var GRID_COLS = 6;
var GRID_ROWS = 7;
var CELL_SIZE = 200; // px
var GRID_OFFSET_X = Math.floor((2048 - GRID_COLS * CELL_SIZE) / 2);
var GRID_OFFSET_Y = 350;
var COLORS = [0, 1, 2, 3, 4]; // 0=red, 1=green, 2=blue, 3=yellow, 4=purple
var COLOR_NAMES = ['red', 'green', 'blue', 'yellow', 'purple', 'white'];
var MAX_COLOR_INDEX = 5; // 0-4 normal, 5=white (final, can't merge further)
var BLOCK_DROP_Y = 2200; // y position for blocks that fall off
// --- Game State ---
var grid = []; // 2D array [col][row] of Block or null
for (var c = 0; c < GRID_COLS; c++) {
grid[c] = [];
for (var r = 0; r < GRID_ROWS; r++) {
grid[c][r] = null;
}
}
var draggingBlock = null;
var dragOffsetX = 0;
var dragOffsetY = 0;
var nextBlocks = []; // Array of next blocks to place
var score = 0;
var scoreTxt = null;
var isProcessing = false; // Prevent input during merges
// --- GUI Elements ---
scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Draw Happy Skybox Background Gradient ---
var skybox = LK.getAsset('block_blue', {
width: 2048,
height: 2732,
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
alpha: 1
});
game.addChild(skybox);
// Add a white "sun" circle for extra cuteness
var sun = LK.getAsset('block_white', {
width: 420,
height: 420,
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 180,
alpha: 0.18
});
game.addChild(sun);
// Add some "clouds" using white blocks with low alpha
for (var i = 0; i < 4; i++) {
var cloud = LK.getAsset('block_white', {
width: 320 + Math.random() * 120,
height: 90 + Math.random() * 40,
anchorX: 0.5,
anchorY: 0.5,
x: 300 + i * 420 + Math.random() * 80,
y: 120 + Math.random() * 60,
alpha: 0.10 + Math.random() * 0.08
});
game.addChild(cloud);
}
// --- Draw Grid Background ---
for (var c = 0; c < GRID_COLS; c++) {
for (var r = 0; r < GRID_ROWS; r++) {
var cell = LK.getAsset('block_white', {
anchorX: 0.5,
anchorY: 0.5,
x: GRID_OFFSET_X + c * CELL_SIZE + CELL_SIZE / 2,
y: GRID_OFFSET_Y + r * CELL_SIZE + CELL_SIZE / 2,
alpha: 0.08
});
game.addChild(cell);
}
}
// --- Helper Functions ---
// Get grid cell from x,y (game coordinates)
function getGridCellFromPos(x, y) {
var gx = Math.floor((x - GRID_OFFSET_X) / CELL_SIZE);
var gy = Math.floor((y - GRID_OFFSET_Y) / CELL_SIZE);
if (gx < 0 || gx >= GRID_COLS || gy < 0 || gy >= GRID_ROWS) return null;
return {
col: gx,
row: gy
};
}
// Get position (x,y) for grid cell
function getPosForCell(col, row) {
return {
x: GRID_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2,
y: GRID_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2
};
}
// Generate a random color index (0-4)
function randomColorIndex() {
return COLORS[Math.floor(Math.random() * COLORS.length)];
}
// Create a new block for the "next" area
function createNextBlock(idx) {
var block = new Block();
block.setColor(idx);
block.x = 2048 / 2 + (nextBlocks.length - 1) * 220;
block.y = 220;
block.scaleX = block.scaleY = 1;
block.isOnGrid = false;
block.gridX = -1;
block.gridY = -1;
game.addChild(block);
return block;
}
// Fill nextBlocks to always have 3 blocks
function refillNextBlocks() {
while (nextBlocks.length < 3) {
var idx = randomColorIndex();
var block = createNextBlock(idx);
nextBlocks.push(block);
}
// Position them nicely
for (var i = 0; i < nextBlocks.length; i++) {
var bx = 2048 / 2 + (i - 1) * 220;
tween(nextBlocks[i], {
x: bx,
y: 220
}, {
duration: 180,
easing: tween.easeInOut
});
}
}
// Remove a block from nextBlocks and shift others
function popNextBlock(block) {
var idx = nextBlocks.indexOf(block);
if (idx >= 0) {
nextBlocks.splice(idx, 1);
}
refillNextBlocks();
}
// Place block on grid
function placeBlockOnGrid(block, col, row) {
block.isOnGrid = true;
block.gridX = col;
block.gridY = row;
grid[col][row] = block;
var pos = getPosForCell(col, row);
tween(block, {
x: pos.x,
y: pos.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeOut
});
}
// Remove block from grid
function removeBlockFromGrid(col, row) {
var block = grid[col][row];
if (block) {
grid[col][row] = null;
}
}
// Find all connected blocks of the same color (DFS)
function findConnectedBlocks(col, row, colorIndex, visited) {
if (col < 0 || col >= GRID_COLS || row < 0 || row >= GRID_ROWS) return [];
if (visited[col + "," + row]) return [];
var block = grid[col][row];
if (!block || block.colorIndex !== colorIndex) return [];
visited[col + "," + row] = true;
var result = [{
col: col,
row: row,
block: block
}];
// 4 directions
var dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
for (var i = 0; i < dirs.length; i++) {
var nc = col + dirs[i][0];
var nr = row + dirs[i][1];
result = result.concat(findConnectedBlocks(nc, nr, colorIndex, visited));
}
return result;
}
// Check if any moves are possible (empty cell exists)
function hasMoves() {
for (var c = 0; c < GRID_COLS; c++) {
for (var r = 0; r < GRID_ROWS; r++) {
if (!grid[c][r]) return true;
}
}
return false;
}
// --- Game Logic ---
// Handle block drop onto grid
function tryPlaceBlock(block, x, y) {
var cell = getGridCellFromPos(x, y);
if (!cell) return false;
var col = cell.col,
row = cell.row;
if (grid[col][row]) return false; // occupied
// Place block
placeBlockOnGrid(block, col, row);
popNextBlock(block);
// After placement, check for merges
processMerges(col, row, function () {
// After all merges and cascades, check for game over
if (!hasMoves()) {
LK.showGameOver();
}
});
return true;
}
// Process merges and cascades starting from (col,row)
function processMerges(col, row, onFinish) {
if (isProcessing) return;
isProcessing = true;
var block = grid[col][row];
if (!block) {
isProcessing = false;
if (onFinish) onFinish();
return;
}
var colorIdx = block.colorIndex;
if (colorIdx >= MAX_COLOR_INDEX) {
isProcessing = false;
if (onFinish) onFinish();
return;
}
// Find all connected blocks of same color
var connected = findConnectedBlocks(col, row, colorIdx, {});
if (connected.length >= 3) {
// Play happy merge sound only on actual merge and upgrade
LK.getSound('merge').play();
// Merge!
for (var i = 0; i < connected.length; i++) {
var b = connected[i].block;
if (b !== block) {
// Animate and remove
(function (bref) {
tween(bref, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 120,
easing: tween.easeIn,
onFinish: function onFinish() {
bref.destroy();
}
});
})(b);
removeBlockFromGrid(b.gridX, b.gridY);
}
}
// Upgrade block color
var newColor = colorIdx + 1;
if (newColor > MAX_COLOR_INDEX) newColor = MAX_COLOR_INDEX;
block.setColor(newColor);
block.animateMerge(function () {
// After animation, check for further merges (cascade)
// We need to check if the upgraded block can merge again
LK.setTimeout(function () {
// Find new connected blocks after upgrade
var newConnected = findConnectedBlocks(col, row, block.colorIndex, {});
if (newConnected.length >= 3) {
// Chain reaction: process again
processMerges(col, row, onFinish);
} else {
// No more merges, finish
isProcessing = false;
if (onFinish) onFinish();
}
}, 180);
});
// Update score
var points = 10 * connected.length * (colorIdx + 1);
score += points;
scoreTxt.setText(score);
} else {
isProcessing = false;
if (onFinish) onFinish();
}
}
// --- Input Handling ---
// Only allow dragging from nextBlocks
game.down = function (x, y, obj) {
if (isProcessing) return;
for (var i = 0; i < nextBlocks.length; i++) {
var block = nextBlocks[i];
// Check if touch is inside block
var dx = x - block.x;
var dy = y - block.y;
if (Math.abs(dx) < 90 && Math.abs(dy) < 90) {
draggingBlock = block;
dragOffsetX = dx;
dragOffsetY = dy;
// Play a sound when picking up a block
LK.getSound('merge').play();
// Bring to front
game.addChild(block);
tween(block, {
scaleX: 1.15,
scaleY: 1.15
}, {
duration: 80,
easing: tween.easeOut
});
break;
}
}
};
game.move = function (x, y, obj) {
if (!draggingBlock) return;
draggingBlock.x = x - dragOffsetX;
draggingBlock.y = y - dragOffsetY;
};
game.up = function (x, y, obj) {
if (!draggingBlock) return;
// Try to place on grid
var placed = tryPlaceBlock(draggingBlock, draggingBlock.x, draggingBlock.y);
if (!placed) {
// Snap back to next area
var idx = nextBlocks.indexOf(draggingBlock);
var bx = 2048 / 2 + (idx - 1) * 220;
tween(draggingBlock, {
x: bx,
y: 220,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeInOut
});
} else {
// Play a sound when block is placed on the grid
LK.getSound('merge').play();
// Block is now on grid, don't need to do anything
}
draggingBlock = null;
};
// --- Game Update Loop ---
game.update = function () {
// Animate blocks falling off (if any)
for (var c = 0; c < GRID_COLS; c++) {
for (var r = 0; r < GRID_ROWS; r++) {
var block = grid[c][r];
if (block && block.y > BLOCK_DROP_Y) {
block.destroy();
grid[c][r] = null;
}
}
}
};
// --- Game Start ---
score = 0;
scoreTxt.setText(score);
refillNextBlocks();
// --- Game Over / Reset handled by LK engine --- /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Block class: represents a block on the grid or in the draggable area
var Block = Container.expand(function () {
var self = Container.call(this);
// Block color index: 0=red, 1=green, 2=blue, 3=yellow, 4=purple, 5=white
self.colorIndex = 0;
self.gridX = -1; // grid position, -1 if not on grid
self.gridY = -1;
self.isOnGrid = false;
// Attach asset based on colorIndex
self.setColor = function (idx) {
self.colorIndex = idx;
if (self.blockAsset) {
self.removeChild(self.blockAsset);
}
if (self.faceAsset) {
self.removeChild(self.faceAsset);
}
var colorNames = ['red', 'green', 'blue', 'yellow', 'purple', 'white'];
var assetId = 'block_' + colorNames[idx];
self.blockAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// --- Add 3D-like shading and highlights ---
// Main highlight (top left)
var highlight = LK.getAsset('block_white', {
width: 60,
height: 60,
anchorX: 0.2,
anchorY: 0.2,
x: -36,
y: -36,
alpha: 0.18
});
self.addChild(highlight);
// Side shadow (bottom right)
var shadow = LK.getAsset('block_black', {
width: 80,
height: 80,
anchorX: 0.8,
anchorY: 0.8,
x: 36,
y: 36,
alpha: 0.10
});
self.addChild(shadow);
// Edge highlight (top edge)
var edgeHighlight = LK.getAsset('block_white', {
width: 80,
height: 16,
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: -44,
alpha: 0.10
});
self.addChild(edgeHighlight);
// Edge shadow (bottom edge)
var edgeShadow = LK.getAsset('block_black', {
width: 80,
height: 16,
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 44,
alpha: 0.08
});
self.addChild(edgeShadow);
// Add a cute manga face overlay (big manga eyes, highlights, and a bigger smile)
self.faceAsset = new Container();
// Manga left eye (big white)
var eyeL = LK.getAsset('block_white', {
width: 36,
height: 36,
anchorX: 0.5,
anchorY: 0.5,
x: -36,
y: -22,
alpha: 0.98
});
// Manga right eye (big white)
var eyeR = LK.getAsset('block_white', {
width: 36,
height: 36,
anchorX: 0.5,
anchorY: 0.5,
x: 36,
y: -22,
alpha: 0.98
});
// Left eye pupil (black)
var pupilL = LK.getAsset('block_black', {
width: 16,
height: 16,
anchorX: 0.5,
anchorY: 0.5,
x: -36,
y: -22,
alpha: 0.95
});
// Right eye pupil (black)
var pupilR = LK.getAsset('block_black', {
width: 16,
height: 16,
anchorX: 0.5,
anchorY: 0.5,
x: 36,
y: -22,
alpha: 0.95
});
// Left eye highlight (white)
var eyeHL = LK.getAsset('block_white', {
width: 7,
height: 7,
anchorX: 0.5,
anchorY: 0.5,
x: -40,
y: -28,
alpha: 0.85
});
// Right eye highlight (white)
var eyeHR = LK.getAsset('block_white', {
width: 7,
height: 7,
anchorX: 0.5,
anchorY: 0.5,
x: 32,
y: -28,
alpha: 0.85
});
// Cheeks
var cheekL = LK.getAsset('block_red', {
width: 18,
height: 10,
anchorX: 0.5,
anchorY: 0.5,
x: -38,
y: 18,
alpha: 0.18
});
var cheekR = LK.getAsset('block_red', {
width: 18,
height: 10,
anchorX: 0.5,
anchorY: 0.5,
x: 38,
y: 18,
alpha: 0.18
});
// Big smile
var smileAssetId = typeof LK.assets !== "undefined" && LK.assets && typeof LK.assets['block_black'] !== "undefined" ? 'block_black' : 'block_blue';
var smile = LK.getAsset(smileAssetId, {
width: 60,
height: 16,
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 22,
alpha: 0.8,
rotation: 0.08
});
// Smile highlight (white, for manga shine)
var smileHL = LK.getAsset('block_white', {
width: 18,
height: 4,
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 18,
alpha: 0.18,
rotation: 0.08
});
self.faceAsset.addChild(eyeL);
self.faceAsset.addChild(eyeR);
self.faceAsset.addChild(pupilL);
self.faceAsset.addChild(pupilR);
self.faceAsset.addChild(eyeHL);
self.faceAsset.addChild(eyeHR);
self.faceAsset.addChild(cheekL);
self.faceAsset.addChild(cheekR);
self.faceAsset.addChild(smile);
self.faceAsset.addChild(smileHL);
self.addChild(self.faceAsset);
};
self.setColor(0);
// Animate merge
self.animateMerge = function (_onFinish) {
tween(self, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn,
onFinish: _onFinish
});
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222233
});
/****
* Game Code
****/
// Sound for merge
// Merged block (final color)
// We'll use 5 colors: red, green, blue, yellow, purple
// Define block shapes for each color and merged color
// --- Game Constants ---
var GRID_COLS = 6;
var GRID_ROWS = 7;
var CELL_SIZE = 200; // px
var GRID_OFFSET_X = Math.floor((2048 - GRID_COLS * CELL_SIZE) / 2);
var GRID_OFFSET_Y = 350;
var COLORS = [0, 1, 2, 3, 4]; // 0=red, 1=green, 2=blue, 3=yellow, 4=purple
var COLOR_NAMES = ['red', 'green', 'blue', 'yellow', 'purple', 'white'];
var MAX_COLOR_INDEX = 5; // 0-4 normal, 5=white (final, can't merge further)
var BLOCK_DROP_Y = 2200; // y position for blocks that fall off
// --- Game State ---
var grid = []; // 2D array [col][row] of Block or null
for (var c = 0; c < GRID_COLS; c++) {
grid[c] = [];
for (var r = 0; r < GRID_ROWS; r++) {
grid[c][r] = null;
}
}
var draggingBlock = null;
var dragOffsetX = 0;
var dragOffsetY = 0;
var nextBlocks = []; // Array of next blocks to place
var score = 0;
var scoreTxt = null;
var isProcessing = false; // Prevent input during merges
// --- GUI Elements ---
scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Draw Happy Skybox Background Gradient ---
var skybox = LK.getAsset('block_blue', {
width: 2048,
height: 2732,
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
alpha: 1
});
game.addChild(skybox);
// Add a white "sun" circle for extra cuteness
var sun = LK.getAsset('block_white', {
width: 420,
height: 420,
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 180,
alpha: 0.18
});
game.addChild(sun);
// Add some "clouds" using white blocks with low alpha
for (var i = 0; i < 4; i++) {
var cloud = LK.getAsset('block_white', {
width: 320 + Math.random() * 120,
height: 90 + Math.random() * 40,
anchorX: 0.5,
anchorY: 0.5,
x: 300 + i * 420 + Math.random() * 80,
y: 120 + Math.random() * 60,
alpha: 0.10 + Math.random() * 0.08
});
game.addChild(cloud);
}
// --- Draw Grid Background ---
for (var c = 0; c < GRID_COLS; c++) {
for (var r = 0; r < GRID_ROWS; r++) {
var cell = LK.getAsset('block_white', {
anchorX: 0.5,
anchorY: 0.5,
x: GRID_OFFSET_X + c * CELL_SIZE + CELL_SIZE / 2,
y: GRID_OFFSET_Y + r * CELL_SIZE + CELL_SIZE / 2,
alpha: 0.08
});
game.addChild(cell);
}
}
// --- Helper Functions ---
// Get grid cell from x,y (game coordinates)
function getGridCellFromPos(x, y) {
var gx = Math.floor((x - GRID_OFFSET_X) / CELL_SIZE);
var gy = Math.floor((y - GRID_OFFSET_Y) / CELL_SIZE);
if (gx < 0 || gx >= GRID_COLS || gy < 0 || gy >= GRID_ROWS) return null;
return {
col: gx,
row: gy
};
}
// Get position (x,y) for grid cell
function getPosForCell(col, row) {
return {
x: GRID_OFFSET_X + col * CELL_SIZE + CELL_SIZE / 2,
y: GRID_OFFSET_Y + row * CELL_SIZE + CELL_SIZE / 2
};
}
// Generate a random color index (0-4)
function randomColorIndex() {
return COLORS[Math.floor(Math.random() * COLORS.length)];
}
// Create a new block for the "next" area
function createNextBlock(idx) {
var block = new Block();
block.setColor(idx);
block.x = 2048 / 2 + (nextBlocks.length - 1) * 220;
block.y = 220;
block.scaleX = block.scaleY = 1;
block.isOnGrid = false;
block.gridX = -1;
block.gridY = -1;
game.addChild(block);
return block;
}
// Fill nextBlocks to always have 3 blocks
function refillNextBlocks() {
while (nextBlocks.length < 3) {
var idx = randomColorIndex();
var block = createNextBlock(idx);
nextBlocks.push(block);
}
// Position them nicely
for (var i = 0; i < nextBlocks.length; i++) {
var bx = 2048 / 2 + (i - 1) * 220;
tween(nextBlocks[i], {
x: bx,
y: 220
}, {
duration: 180,
easing: tween.easeInOut
});
}
}
// Remove a block from nextBlocks and shift others
function popNextBlock(block) {
var idx = nextBlocks.indexOf(block);
if (idx >= 0) {
nextBlocks.splice(idx, 1);
}
refillNextBlocks();
}
// Place block on grid
function placeBlockOnGrid(block, col, row) {
block.isOnGrid = true;
block.gridX = col;
block.gridY = row;
grid[col][row] = block;
var pos = getPosForCell(col, row);
tween(block, {
x: pos.x,
y: pos.y,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeOut
});
}
// Remove block from grid
function removeBlockFromGrid(col, row) {
var block = grid[col][row];
if (block) {
grid[col][row] = null;
}
}
// Find all connected blocks of the same color (DFS)
function findConnectedBlocks(col, row, colorIndex, visited) {
if (col < 0 || col >= GRID_COLS || row < 0 || row >= GRID_ROWS) return [];
if (visited[col + "," + row]) return [];
var block = grid[col][row];
if (!block || block.colorIndex !== colorIndex) return [];
visited[col + "," + row] = true;
var result = [{
col: col,
row: row,
block: block
}];
// 4 directions
var dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
for (var i = 0; i < dirs.length; i++) {
var nc = col + dirs[i][0];
var nr = row + dirs[i][1];
result = result.concat(findConnectedBlocks(nc, nr, colorIndex, visited));
}
return result;
}
// Check if any moves are possible (empty cell exists)
function hasMoves() {
for (var c = 0; c < GRID_COLS; c++) {
for (var r = 0; r < GRID_ROWS; r++) {
if (!grid[c][r]) return true;
}
}
return false;
}
// --- Game Logic ---
// Handle block drop onto grid
function tryPlaceBlock(block, x, y) {
var cell = getGridCellFromPos(x, y);
if (!cell) return false;
var col = cell.col,
row = cell.row;
if (grid[col][row]) return false; // occupied
// Place block
placeBlockOnGrid(block, col, row);
popNextBlock(block);
// After placement, check for merges
processMerges(col, row, function () {
// After all merges and cascades, check for game over
if (!hasMoves()) {
LK.showGameOver();
}
});
return true;
}
// Process merges and cascades starting from (col,row)
function processMerges(col, row, onFinish) {
if (isProcessing) return;
isProcessing = true;
var block = grid[col][row];
if (!block) {
isProcessing = false;
if (onFinish) onFinish();
return;
}
var colorIdx = block.colorIndex;
if (colorIdx >= MAX_COLOR_INDEX) {
isProcessing = false;
if (onFinish) onFinish();
return;
}
// Find all connected blocks of same color
var connected = findConnectedBlocks(col, row, colorIdx, {});
if (connected.length >= 3) {
// Play happy merge sound only on actual merge and upgrade
LK.getSound('merge').play();
// Merge!
for (var i = 0; i < connected.length; i++) {
var b = connected[i].block;
if (b !== block) {
// Animate and remove
(function (bref) {
tween(bref, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 120,
easing: tween.easeIn,
onFinish: function onFinish() {
bref.destroy();
}
});
})(b);
removeBlockFromGrid(b.gridX, b.gridY);
}
}
// Upgrade block color
var newColor = colorIdx + 1;
if (newColor > MAX_COLOR_INDEX) newColor = MAX_COLOR_INDEX;
block.setColor(newColor);
block.animateMerge(function () {
// After animation, check for further merges (cascade)
// We need to check if the upgraded block can merge again
LK.setTimeout(function () {
// Find new connected blocks after upgrade
var newConnected = findConnectedBlocks(col, row, block.colorIndex, {});
if (newConnected.length >= 3) {
// Chain reaction: process again
processMerges(col, row, onFinish);
} else {
// No more merges, finish
isProcessing = false;
if (onFinish) onFinish();
}
}, 180);
});
// Update score
var points = 10 * connected.length * (colorIdx + 1);
score += points;
scoreTxt.setText(score);
} else {
isProcessing = false;
if (onFinish) onFinish();
}
}
// --- Input Handling ---
// Only allow dragging from nextBlocks
game.down = function (x, y, obj) {
if (isProcessing) return;
for (var i = 0; i < nextBlocks.length; i++) {
var block = nextBlocks[i];
// Check if touch is inside block
var dx = x - block.x;
var dy = y - block.y;
if (Math.abs(dx) < 90 && Math.abs(dy) < 90) {
draggingBlock = block;
dragOffsetX = dx;
dragOffsetY = dy;
// Play a sound when picking up a block
LK.getSound('merge').play();
// Bring to front
game.addChild(block);
tween(block, {
scaleX: 1.15,
scaleY: 1.15
}, {
duration: 80,
easing: tween.easeOut
});
break;
}
}
};
game.move = function (x, y, obj) {
if (!draggingBlock) return;
draggingBlock.x = x - dragOffsetX;
draggingBlock.y = y - dragOffsetY;
};
game.up = function (x, y, obj) {
if (!draggingBlock) return;
// Try to place on grid
var placed = tryPlaceBlock(draggingBlock, draggingBlock.x, draggingBlock.y);
if (!placed) {
// Snap back to next area
var idx = nextBlocks.indexOf(draggingBlock);
var bx = 2048 / 2 + (idx - 1) * 220;
tween(draggingBlock, {
x: bx,
y: 220,
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeInOut
});
} else {
// Play a sound when block is placed on the grid
LK.getSound('merge').play();
// Block is now on grid, don't need to do anything
}
draggingBlock = null;
};
// --- Game Update Loop ---
game.update = function () {
// Animate blocks falling off (if any)
for (var c = 0; c < GRID_COLS; c++) {
for (var r = 0; r < GRID_ROWS; r++) {
var block = grid[c][r];
if (block && block.y > BLOCK_DROP_Y) {
block.destroy();
grid[c][r] = null;
}
}
}
};
// --- Game Start ---
score = 0;
scoreTxt.setText(score);
refillNextBlocks();
// --- Game Over / Reset handled by LK engine ---