/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Block class: a draggable block with a given shape and color var Block = Container.expand(function () { var self = Container.call(this); // Properties self.shape = null; // {cells, size} self.color = null; // asset id self.cellSize = 210; self.cells = []; // graphical cell assets self.ghostCells = []; // for ghost preview self.isDragging = false; self.gridX = null; // grid position if placed self.gridY = null; // Set up block with shape and color self.setup = function (shape, color) { self.shape = shape; self.color = color; // Remove old cells for (var i = 0; i < self.cells.length; ++i) self.removeChild(self.cells[i]); self.cells = []; // Add new cells for (var i = 0; i < shape.cells.length; ++i) { var c = shape.cells[i]; var cell = self.attachAsset(color, { anchorX: 0.5, anchorY: 0.5, x: c[0] * self.cellSize, y: c[1] * self.cellSize }); self.cells.push(cell); } // Center anchor var w = shape.size[0] * self.cellSize, h = shape.size[1] * self.cellSize; self.pivot.x = w / 2 - self.cellSize / 2; self.pivot.y = h / 2 - self.cellSize / 2; }; // Show ghost preview at given grid position self.showGhost = function (gridX, gridY, grid) { self.hideGhost(); for (var i = 0; i < self.shape.cells.length; ++i) { var c = self.shape.cells[i]; var gx = gridX + c[0], gy = gridY + c[1]; if (gx < 0 || gx >= grid.cols || gy < 0 || gy >= grid.rows) continue; var ghost = LK.getAsset(self.color, { anchorX: 0.5, anchorY: 0.5, x: grid.gridOriginX + gx * self.cellSize + self.cellSize / 2, y: grid.gridOriginY + gy * self.cellSize + self.cellSize / 2, alpha: 0.7 // More visible ghost }); ghost.tint = 0xffffff; // Add white tint for extra contrast self.ghostCells.push(ghost); grid.addChild(ghost); } }; self.hideGhost = function () { for (var i = 0; i < self.ghostCells.length; ++i) { if (self.ghostCells[i].parent) self.ghostCells[i].parent.removeChild(self.ghostCells[i]); } self.ghostCells = []; }; // Animate block on placement self.animatePlace = function () { for (var i = 0; i < self.cells.length; ++i) { var cell = self.cells[i]; cell.scaleX = 1.2; cell.scaleY = 1.2; tween(cell, { scaleX: 1, scaleY: 1 }, { duration: 120, easing: tween.easeOut }); } }; return self; }); // Grid class: the main play area var Grid = Container.expand(function () { var self = Container.call(this); self.rows = 9; self.cols = 9; self.cellSize = 210; self.grid = []; // 2D array: grid[y][x] = {block: Block, color: assetId} self.cellNodes = []; // graphical cell backgrounds self.gridOriginX = 0; self.gridOriginY = 0; // Set up grid self.setup = function () { // Center grid with padding to ensure full visibility var totalW = self.cols * self.cellSize; var totalH = self.rows * self.cellSize; var paddingX = 100; // Padding to keep away from edges var paddingY = 300; // Extra padding for tray area at bottom self.gridOriginX = Math.floor((2048 - totalW) / 2); self.gridOriginY = Math.floor((2732 - totalH - paddingY) / 2) + paddingY / 2; // Draw cells for (var y = 0; y < self.rows; ++y) { self.grid[y] = []; self.cellNodes[y] = []; for (var x = 0; x < self.cols; ++x) { self.grid[y][x] = null; var cell = self.attachAsset('cell', { anchorX: 0.5, anchorY: 0.5, x: self.gridOriginX + x * self.cellSize + self.cellSize / 2, y: self.gridOriginY + y * self.cellSize + self.cellSize / 2, scaleX: 0.95, // Scale down to make guidelines thinner scaleY: 0.95 // Scale down to make guidelines thinner }); self.cellNodes[y][x] = cell; } } }; // Check if a block can be placed at gridX,gridY self.canPlace = function (block, gridX, gridY) { for (var i = 0; i < block.shape.cells.length; ++i) { var c = block.shape.cells[i]; var x = gridX + c[0], y = gridY + c[1]; if (x < 0 || x >= self.cols || y < 0 || y >= self.rows) return false; if (self.grid[y][x]) return false; } return true; }; // Place a block at gridX,gridY self.placeBlock = function (block, gridX, gridY) { for (var i = 0; i < block.shape.cells.length; ++i) { var c = block.shape.cells[i]; var x = gridX + c[0], y = gridY + c[1]; self.grid[y][x] = { color: block.color }; // Animate cell var cell = self.cellNodes[y][x]; cell.scaleX = 1.2; cell.scaleY = 1.2; // Set tint to the color value of the asset if (block.color === 'block_blue') cell.tint = 0x3b82f6;else if (block.color === 'block_red') cell.tint = 0xef4444;else if (block.color === 'block_green') cell.tint = 0x22c55e;else if (block.color === 'block_yellow') cell.tint = 0xfacc15;else if (block.color === 'block_purple') cell.tint = 0xa21caf;else if (block.color === 'block_orange') cell.tint = 0xf97316; tween(cell, { scaleX: 1, scaleY: 1 }, { duration: 120, easing: tween.easeOut }); } }; // Clear full rows/columns, return number of cleared lines self.clearLines = function () { var cleared = 0; var toClearRows = []; var toClearCols = []; // Check rows for (var y = 0; y < self.rows; ++y) { var full = true; for (var x = 0; x < self.cols; ++x) { if (!self.grid[y][x]) { full = false; break; } } if (full) toClearRows.push(y); } // Check columns for (var x = 0; x < self.cols; ++x) { var full = true; for (var y = 0; y < self.rows; ++y) { if (!self.grid[y][x]) { full = false; break; } } if (full) toClearCols.push(x); } // Clear rows for (var i = 0; i < toClearRows.length; ++i) { var y = toClearRows[i]; for (var x = 0; x < self.cols; ++x) { self.grid[y][x] = null; var cell = self.cellNodes[y][x]; cell.tint = 0xe0e0e0; // Animate cell.alpha = 0.2; tween(cell, { alpha: 1 }, { duration: 200, easing: tween.easeOut }); } cleared++; } // Clear columns for (var i = 0; i < toClearCols.length; ++i) { var x = toClearCols[i]; for (var y = 0; y < self.rows; ++y) { self.grid[y][x] = null; var cell = self.cellNodes[y][x]; cell.tint = 0xe0e0e0; cell.alpha = 0.2; tween(cell, { alpha: 1 }, { duration: 200, easing: tween.easeOut }); } cleared++; } return cleared; }; // Check if any of the given blocks can be placed anywhere self.anyValidPlacement = function (blocks) { for (var b = 0; b < blocks.length; ++b) { var block = blocks[b]; for (var y = 0; y <= self.rows - block.shape.size[1]; ++y) { for (var x = 0; x <= self.cols - block.shape.size[0]; ++x) { if (self.canPlace(block, x, y)) return true; } } } return false; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x0a174e }); /**** * Game Code ****/ // --- Game variables --- /* We use only shapes for blocks and grid cells. Each block color is a unique asset. Block shapes: 1x1, 1x2, 2x1, 2x2, 1x3, 3x1, L-shape (2x2 with one missing), etc. Colors: blue, red, green, yellow, purple, orange. Grid cell: light gray. */ // Block shapes definition var BLOCK_SHAPES = [ // Each shape: {cells: [[x,y],...], size: [w,h]} { cells: [[0, 0]], size: [1, 1] }, // single { cells: [[0, 0], [1, 0]], size: [2, 1] }, // 1x2 horizontal { cells: [[0, 0], [0, 1]], size: [1, 2] }, // 2x1 vertical { cells: [[0, 0], [1, 0], [0, 1], [1, 1]], size: [2, 2] }, // 2x2 { cells: [[0, 0], [1, 0], [2, 0]], size: [3, 1] }, // 1x3 horizontal { cells: [[0, 0], [0, 1], [0, 2]], size: [1, 3] }, // 3x1 vertical { cells: [[0, 0], [1, 0], [0, 1]], size: [2, 2] }, // L-shape (top-left) { cells: [[1, 0], [0, 1], [1, 1]], size: [2, 2] }, // L-shape (top-right) { cells: [[0, 0], [1, 0], [1, 1]], size: [2, 2] }, // L-shape (bottom-right) { cells: [[0, 0], [0, 1], [1, 1]], size: [2, 2] } // L-shape (bottom-left) ]; // Block colors var BLOCK_COLORS = ['block_blue', 'block_red', 'block_green', 'block_yellow', 'block_purple', 'block_orange']; // Utility: pick random element from array function randArr(arr) { return arr[Math.floor(Math.random() * arr.length)]; } var grid = new Grid(); grid.setup(); game.addChild(grid); var score = LK.getScore(); var scoreTxt = new Text2('0', { size: 120, fill: 0x222222 }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Block tray: 3 blocks to choose from var trayBlocks = []; var trayY = 2732 - 220; var trayX0 = 2048 / 2 - 2 * 270; var traySpacing = 360; // Drag state var dragBlock = null; var dragOffsetX = 0; var dragOffsetY = 0; // Helper: create a new random block function createRandomBlock() { var block = new Block(); var shape = randArr(BLOCK_SHAPES); var color = randArr(BLOCK_COLORS); block.setup(shape, color); return block; } // Helper: refill tray with up to 3 blocks function refillTray() { // Remove old blocks for (var i = 0; i < trayBlocks.length; ++i) { if (trayBlocks[i].parent) trayBlocks[i].parent.removeChild(trayBlocks[i]); } trayBlocks = []; for (var i = 0; i < 3; ++i) { var block = createRandomBlock(); block.x = trayX0 + i * traySpacing; block.y = trayY; block.scaleX = block.scaleY = 1.1; trayBlocks.push(block); game.addChild(block); } } // Helper: update score display function updateScore(val) { LK.setScore(val); score = LK.getScore(); scoreTxt.setText(score); } // Helper: get grid cell under (x, y) in game coordinates function getGridCellAt(x, y) { var gx = Math.floor((x - grid.gridOriginX) / grid.cellSize); var gy = Math.floor((y - grid.gridOriginY) / grid.cellSize); if (gx < 0 || gx >= grid.cols || gy < 0 || gy >= grid.rows) return null; return { x: gx, y: gy }; } // Helper: snap block to tray function snapBlockToTray(block) { var idx = trayBlocks.indexOf(block); if (idx >= 0) { block.x = trayX0 + idx * traySpacing; block.y = trayY; block.scaleX = block.scaleY = 1.1; block.zIndex = 0; } } // --- Input handlers --- // Find block in tray under pointer function blockUnderPointer(x, y) { for (var i = 0; i < trayBlocks.length; ++i) { var block = trayBlocks[i]; // Check bounding box var bx = block.x - block.pivot.x * block.scaleX; var by = block.y - block.pivot.y * block.scaleY; var bw = block.shape.size[0] * block.cellSize * block.scaleX; var bh = block.shape.size[1] * block.cellSize * block.scaleY; if (x >= bx && x <= bx + bw && y >= by && y <= by + bh) { return block; } } return null; } // Drag start game.down = function (x, y, obj) { if (dragBlock) return; var block = blockUnderPointer(x, y); if (block) { dragBlock = block; dragBlock.isDragging = true; dragBlock.zIndex = 100; dragOffsetX = x - dragBlock.x; dragOffsetY = y - dragBlock.y; // Animate up tween(dragBlock, { scaleX: 1.2, scaleY: 1.2 }, { duration: 80, easing: tween.easeOut }); } }; // Drag move game.move = function (x, y, obj) { if (!dragBlock) return; dragBlock.x = x - dragOffsetX; dragBlock.y = y - dragOffsetY; // Show ghost preview if over grid dragBlock.hideGhost(); var cell = getGridCellAt(x, y); if (cell) { var can = grid.canPlace(dragBlock, cell.x, cell.y); if (can) { dragBlock.showGhost(cell.x, cell.y, grid); } } }; // Drag end game.up = function (x, y, obj) { if (!dragBlock) return; // Try to find the nearest valid cell to drop the block, even if not perfectly centered var cell = getGridCellAt(x, y); var placed = false; var foundCell = null; if (cell) { // Try the cell under pointer first if (grid.canPlace(dragBlock, cell.x, cell.y)) { foundCell = cell; } else { // Search nearby cells within a 1-cell radius for a valid placement var minDist = 99999; var bestCell = null; for (var dy = -1; dy <= 1; ++dy) { for (var dx = -1; dx <= 1; ++dx) { var cx = cell.x + dx; var cy = cell.y + dy; if (cx < 0 || cx >= grid.cols || cy < 0 || cy >= grid.rows) continue; if (grid.canPlace(dragBlock, cx, cy)) { // Prefer the closest cell to pointer var dist = Math.abs(dx) + Math.abs(dy); if (dist < minDist) { minDist = dist; bestCell = { x: cx, y: cy }; } } } } if (bestCell) foundCell = bestCell; } } if (foundCell) { // Place block grid.placeBlock(dragBlock, foundCell.x, foundCell.y); dragBlock.animatePlace(); dragBlock.hideGhost(); if (dragBlock.parent) dragBlock.parent.removeChild(dragBlock); var idx = trayBlocks.indexOf(dragBlock); if (idx >= 0) trayBlocks.splice(idx, 1); LK.getSound('place').play(); // Clear lines var cleared = grid.clearLines(); if (cleared > 0) { updateScore(LK.getScore() + cleared * 10); LK.getSound('clear').play(); } else { updateScore(LK.getScore() + 1); } placed = true; // If tray empty, refill if (trayBlocks.length === 0) { refillTray(); } // Check for game over if (!grid.anyValidPlacement(trayBlocks)) { LK.getSound('fail').play(); LK.effects.flashScreen(0xff0000, 800); LK.showGameOver(); return; } } if (!placed) { // Snap back to tray snapBlockToTray(dragBlock); dragBlock.hideGhost(); } // Animate down tween(dragBlock, { scaleX: 1.1, scaleY: 1.1 }, { duration: 80, easing: tween.easeOut }); dragBlock.isDragging = false; dragBlock = null; }; // --- Game update loop --- game.update = function () { // No per-frame logic needed for now }; // --- Start game --- LK.setScore(0); updateScore(0); refillTray();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Block class: a draggable block with a given shape and color
var Block = Container.expand(function () {
var self = Container.call(this);
// Properties
self.shape = null; // {cells, size}
self.color = null; // asset id
self.cellSize = 210;
self.cells = []; // graphical cell assets
self.ghostCells = []; // for ghost preview
self.isDragging = false;
self.gridX = null; // grid position if placed
self.gridY = null;
// Set up block with shape and color
self.setup = function (shape, color) {
self.shape = shape;
self.color = color;
// Remove old cells
for (var i = 0; i < self.cells.length; ++i) self.removeChild(self.cells[i]);
self.cells = [];
// Add new cells
for (var i = 0; i < shape.cells.length; ++i) {
var c = shape.cells[i];
var cell = self.attachAsset(color, {
anchorX: 0.5,
anchorY: 0.5,
x: c[0] * self.cellSize,
y: c[1] * self.cellSize
});
self.cells.push(cell);
}
// Center anchor
var w = shape.size[0] * self.cellSize,
h = shape.size[1] * self.cellSize;
self.pivot.x = w / 2 - self.cellSize / 2;
self.pivot.y = h / 2 - self.cellSize / 2;
};
// Show ghost preview at given grid position
self.showGhost = function (gridX, gridY, grid) {
self.hideGhost();
for (var i = 0; i < self.shape.cells.length; ++i) {
var c = self.shape.cells[i];
var gx = gridX + c[0],
gy = gridY + c[1];
if (gx < 0 || gx >= grid.cols || gy < 0 || gy >= grid.rows) continue;
var ghost = LK.getAsset(self.color, {
anchorX: 0.5,
anchorY: 0.5,
x: grid.gridOriginX + gx * self.cellSize + self.cellSize / 2,
y: grid.gridOriginY + gy * self.cellSize + self.cellSize / 2,
alpha: 0.7 // More visible ghost
});
ghost.tint = 0xffffff; // Add white tint for extra contrast
self.ghostCells.push(ghost);
grid.addChild(ghost);
}
};
self.hideGhost = function () {
for (var i = 0; i < self.ghostCells.length; ++i) {
if (self.ghostCells[i].parent) self.ghostCells[i].parent.removeChild(self.ghostCells[i]);
}
self.ghostCells = [];
};
// Animate block on placement
self.animatePlace = function () {
for (var i = 0; i < self.cells.length; ++i) {
var cell = self.cells[i];
cell.scaleX = 1.2;
cell.scaleY = 1.2;
tween(cell, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeOut
});
}
};
return self;
});
// Grid class: the main play area
var Grid = Container.expand(function () {
var self = Container.call(this);
self.rows = 9;
self.cols = 9;
self.cellSize = 210;
self.grid = []; // 2D array: grid[y][x] = {block: Block, color: assetId}
self.cellNodes = []; // graphical cell backgrounds
self.gridOriginX = 0;
self.gridOriginY = 0;
// Set up grid
self.setup = function () {
// Center grid with padding to ensure full visibility
var totalW = self.cols * self.cellSize;
var totalH = self.rows * self.cellSize;
var paddingX = 100; // Padding to keep away from edges
var paddingY = 300; // Extra padding for tray area at bottom
self.gridOriginX = Math.floor((2048 - totalW) / 2);
self.gridOriginY = Math.floor((2732 - totalH - paddingY) / 2) + paddingY / 2;
// Draw cells
for (var y = 0; y < self.rows; ++y) {
self.grid[y] = [];
self.cellNodes[y] = [];
for (var x = 0; x < self.cols; ++x) {
self.grid[y][x] = null;
var cell = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5,
x: self.gridOriginX + x * self.cellSize + self.cellSize / 2,
y: self.gridOriginY + y * self.cellSize + self.cellSize / 2,
scaleX: 0.95,
// Scale down to make guidelines thinner
scaleY: 0.95 // Scale down to make guidelines thinner
});
self.cellNodes[y][x] = cell;
}
}
};
// Check if a block can be placed at gridX,gridY
self.canPlace = function (block, gridX, gridY) {
for (var i = 0; i < block.shape.cells.length; ++i) {
var c = block.shape.cells[i];
var x = gridX + c[0],
y = gridY + c[1];
if (x < 0 || x >= self.cols || y < 0 || y >= self.rows) return false;
if (self.grid[y][x]) return false;
}
return true;
};
// Place a block at gridX,gridY
self.placeBlock = function (block, gridX, gridY) {
for (var i = 0; i < block.shape.cells.length; ++i) {
var c = block.shape.cells[i];
var x = gridX + c[0],
y = gridY + c[1];
self.grid[y][x] = {
color: block.color
};
// Animate cell
var cell = self.cellNodes[y][x];
cell.scaleX = 1.2;
cell.scaleY = 1.2;
// Set tint to the color value of the asset
if (block.color === 'block_blue') cell.tint = 0x3b82f6;else if (block.color === 'block_red') cell.tint = 0xef4444;else if (block.color === 'block_green') cell.tint = 0x22c55e;else if (block.color === 'block_yellow') cell.tint = 0xfacc15;else if (block.color === 'block_purple') cell.tint = 0xa21caf;else if (block.color === 'block_orange') cell.tint = 0xf97316;
tween(cell, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeOut
});
}
};
// Clear full rows/columns, return number of cleared lines
self.clearLines = function () {
var cleared = 0;
var toClearRows = [];
var toClearCols = [];
// Check rows
for (var y = 0; y < self.rows; ++y) {
var full = true;
for (var x = 0; x < self.cols; ++x) {
if (!self.grid[y][x]) {
full = false;
break;
}
}
if (full) toClearRows.push(y);
}
// Check columns
for (var x = 0; x < self.cols; ++x) {
var full = true;
for (var y = 0; y < self.rows; ++y) {
if (!self.grid[y][x]) {
full = false;
break;
}
}
if (full) toClearCols.push(x);
}
// Clear rows
for (var i = 0; i < toClearRows.length; ++i) {
var y = toClearRows[i];
for (var x = 0; x < self.cols; ++x) {
self.grid[y][x] = null;
var cell = self.cellNodes[y][x];
cell.tint = 0xe0e0e0;
// Animate
cell.alpha = 0.2;
tween(cell, {
alpha: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
cleared++;
}
// Clear columns
for (var i = 0; i < toClearCols.length; ++i) {
var x = toClearCols[i];
for (var y = 0; y < self.rows; ++y) {
self.grid[y][x] = null;
var cell = self.cellNodes[y][x];
cell.tint = 0xe0e0e0;
cell.alpha = 0.2;
tween(cell, {
alpha: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
cleared++;
}
return cleared;
};
// Check if any of the given blocks can be placed anywhere
self.anyValidPlacement = function (blocks) {
for (var b = 0; b < blocks.length; ++b) {
var block = blocks[b];
for (var y = 0; y <= self.rows - block.shape.size[1]; ++y) {
for (var x = 0; x <= self.cols - block.shape.size[0]; ++x) {
if (self.canPlace(block, x, y)) return true;
}
}
}
return false;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x0a174e
});
/****
* Game Code
****/
// --- Game variables ---
/*
We use only shapes for blocks and grid cells. Each block color is a unique asset.
Block shapes: 1x1, 1x2, 2x1, 2x2, 1x3, 3x1, L-shape (2x2 with one missing), etc.
Colors: blue, red, green, yellow, purple, orange.
Grid cell: light gray.
*/
// Block shapes definition
var BLOCK_SHAPES = [
// Each shape: {cells: [[x,y],...], size: [w,h]}
{
cells: [[0, 0]],
size: [1, 1]
},
// single
{
cells: [[0, 0], [1, 0]],
size: [2, 1]
},
// 1x2 horizontal
{
cells: [[0, 0], [0, 1]],
size: [1, 2]
},
// 2x1 vertical
{
cells: [[0, 0], [1, 0], [0, 1], [1, 1]],
size: [2, 2]
},
// 2x2
{
cells: [[0, 0], [1, 0], [2, 0]],
size: [3, 1]
},
// 1x3 horizontal
{
cells: [[0, 0], [0, 1], [0, 2]],
size: [1, 3]
},
// 3x1 vertical
{
cells: [[0, 0], [1, 0], [0, 1]],
size: [2, 2]
},
// L-shape (top-left)
{
cells: [[1, 0], [0, 1], [1, 1]],
size: [2, 2]
},
// L-shape (top-right)
{
cells: [[0, 0], [1, 0], [1, 1]],
size: [2, 2]
},
// L-shape (bottom-right)
{
cells: [[0, 0], [0, 1], [1, 1]],
size: [2, 2]
} // L-shape (bottom-left)
];
// Block colors
var BLOCK_COLORS = ['block_blue', 'block_red', 'block_green', 'block_yellow', 'block_purple', 'block_orange'];
// Utility: pick random element from array
function randArr(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
var grid = new Grid();
grid.setup();
game.addChild(grid);
var score = LK.getScore();
var scoreTxt = new Text2('0', {
size: 120,
fill: 0x222222
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Block tray: 3 blocks to choose from
var trayBlocks = [];
var trayY = 2732 - 220;
var trayX0 = 2048 / 2 - 2 * 270;
var traySpacing = 360;
// Drag state
var dragBlock = null;
var dragOffsetX = 0;
var dragOffsetY = 0;
// Helper: create a new random block
function createRandomBlock() {
var block = new Block();
var shape = randArr(BLOCK_SHAPES);
var color = randArr(BLOCK_COLORS);
block.setup(shape, color);
return block;
}
// Helper: refill tray with up to 3 blocks
function refillTray() {
// Remove old blocks
for (var i = 0; i < trayBlocks.length; ++i) {
if (trayBlocks[i].parent) trayBlocks[i].parent.removeChild(trayBlocks[i]);
}
trayBlocks = [];
for (var i = 0; i < 3; ++i) {
var block = createRandomBlock();
block.x = trayX0 + i * traySpacing;
block.y = trayY;
block.scaleX = block.scaleY = 1.1;
trayBlocks.push(block);
game.addChild(block);
}
}
// Helper: update score display
function updateScore(val) {
LK.setScore(val);
score = LK.getScore();
scoreTxt.setText(score);
}
// Helper: get grid cell under (x, y) in game coordinates
function getGridCellAt(x, y) {
var gx = Math.floor((x - grid.gridOriginX) / grid.cellSize);
var gy = Math.floor((y - grid.gridOriginY) / grid.cellSize);
if (gx < 0 || gx >= grid.cols || gy < 0 || gy >= grid.rows) return null;
return {
x: gx,
y: gy
};
}
// Helper: snap block to tray
function snapBlockToTray(block) {
var idx = trayBlocks.indexOf(block);
if (idx >= 0) {
block.x = trayX0 + idx * traySpacing;
block.y = trayY;
block.scaleX = block.scaleY = 1.1;
block.zIndex = 0;
}
}
// --- Input handlers ---
// Find block in tray under pointer
function blockUnderPointer(x, y) {
for (var i = 0; i < trayBlocks.length; ++i) {
var block = trayBlocks[i];
// Check bounding box
var bx = block.x - block.pivot.x * block.scaleX;
var by = block.y - block.pivot.y * block.scaleY;
var bw = block.shape.size[0] * block.cellSize * block.scaleX;
var bh = block.shape.size[1] * block.cellSize * block.scaleY;
if (x >= bx && x <= bx + bw && y >= by && y <= by + bh) {
return block;
}
}
return null;
}
// Drag start
game.down = function (x, y, obj) {
if (dragBlock) return;
var block = blockUnderPointer(x, y);
if (block) {
dragBlock = block;
dragBlock.isDragging = true;
dragBlock.zIndex = 100;
dragOffsetX = x - dragBlock.x;
dragOffsetY = y - dragBlock.y;
// Animate up
tween(dragBlock, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 80,
easing: tween.easeOut
});
}
};
// Drag move
game.move = function (x, y, obj) {
if (!dragBlock) return;
dragBlock.x = x - dragOffsetX;
dragBlock.y = y - dragOffsetY;
// Show ghost preview if over grid
dragBlock.hideGhost();
var cell = getGridCellAt(x, y);
if (cell) {
var can = grid.canPlace(dragBlock, cell.x, cell.y);
if (can) {
dragBlock.showGhost(cell.x, cell.y, grid);
}
}
};
// Drag end
game.up = function (x, y, obj) {
if (!dragBlock) return;
// Try to find the nearest valid cell to drop the block, even if not perfectly centered
var cell = getGridCellAt(x, y);
var placed = false;
var foundCell = null;
if (cell) {
// Try the cell under pointer first
if (grid.canPlace(dragBlock, cell.x, cell.y)) {
foundCell = cell;
} else {
// Search nearby cells within a 1-cell radius for a valid placement
var minDist = 99999;
var bestCell = null;
for (var dy = -1; dy <= 1; ++dy) {
for (var dx = -1; dx <= 1; ++dx) {
var cx = cell.x + dx;
var cy = cell.y + dy;
if (cx < 0 || cx >= grid.cols || cy < 0 || cy >= grid.rows) continue;
if (grid.canPlace(dragBlock, cx, cy)) {
// Prefer the closest cell to pointer
var dist = Math.abs(dx) + Math.abs(dy);
if (dist < minDist) {
minDist = dist;
bestCell = {
x: cx,
y: cy
};
}
}
}
}
if (bestCell) foundCell = bestCell;
}
}
if (foundCell) {
// Place block
grid.placeBlock(dragBlock, foundCell.x, foundCell.y);
dragBlock.animatePlace();
dragBlock.hideGhost();
if (dragBlock.parent) dragBlock.parent.removeChild(dragBlock);
var idx = trayBlocks.indexOf(dragBlock);
if (idx >= 0) trayBlocks.splice(idx, 1);
LK.getSound('place').play();
// Clear lines
var cleared = grid.clearLines();
if (cleared > 0) {
updateScore(LK.getScore() + cleared * 10);
LK.getSound('clear').play();
} else {
updateScore(LK.getScore() + 1);
}
placed = true;
// If tray empty, refill
if (trayBlocks.length === 0) {
refillTray();
}
// Check for game over
if (!grid.anyValidPlacement(trayBlocks)) {
LK.getSound('fail').play();
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
return;
}
}
if (!placed) {
// Snap back to tray
snapBlockToTray(dragBlock);
dragBlock.hideGhost();
}
// Animate down
tween(dragBlock, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 80,
easing: tween.easeOut
});
dragBlock.isDragging = false;
dragBlock = null;
};
// --- Game update loop ---
game.update = function () {
// No per-frame logic needed for now
};
// --- Start game ---
LK.setScore(0);
updateScore(0);
refillTray();