/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Block class for stackable blocks var Block = Container.expand(function () { var self = Container.call(this); // Attach block asset var blockAsset = self.attachAsset('block', { anchorX: 0.5, anchorY: 0.5 }); // Store reference for resizing self.blockAsset = blockAsset; // Set initial width/height self.setBlockSize = function (width, height) { blockAsset.width = width; blockAsset.height = height; self.width = width; self.height = height; }; // Animate block to a new position (used for dropping) self.animateDrop = function (targetY, onFinish) { tween(self, { y: targetY }, { duration: 180, easing: tween.cubicIn, onFinish: onFinish }); }; // Animate block to a new width (used for trimming) self.animateTrim = function (newWidth, newX, onFinish) { tween(self.blockAsset, { width: newWidth }, { duration: 120, easing: tween.linear }); tween(self, { x: newX }, { duration: 120, easing: tween.linear, onFinish: onFinish }); }; // Set color (for overhangs) self.setColor = function (color) { blockAsset.color = color; }; return self; }); // Overhang class for falling pieces var Overhang = Container.expand(function () { var self = Container.call(this); var overhangAsset = self.attachAsset('overhang', { anchorX: 0.5, anchorY: 0.5 }); self.setOverhangSize = function (width, height) { overhangAsset.width = width; overhangAsset.height = height; self.width = width; self.height = height; }; // Animate falling self.fall = function (onFinish) { tween(self, { y: self.y + 400 }, { duration: 400, easing: tween.cubicIn, onFinish: onFinish }); tween(self, { alpha: 0 }, { duration: 400, easing: tween.linear }); }; // Set color self.setColor = function (color) { overhangAsset.color = color; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222a36 }); /**** * Game Code ****/ // Add background var background = game.attachAsset('background', { anchorX: 0, anchorY: 0, x: 0, y: 0 }); // Scale background to cover full screen background.width = GAME_WIDTH; background.height = GAME_HEIGHT; // Overhang asset: red // Base block asset: darker blue // Block asset: blue rectangle // Game constants var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; var BLOCK_HEIGHT = 100; var BLOCK_START_WIDTH = 600; var BLOCK_MIN_WIDTH = 40; var BLOCK_SPEED_START = 12; var BLOCK_SPEED_INCREMENT = 0.7; var BLOCK_Y_START = GAME_HEIGHT - 400; var BLOCK_Y_GAP = 90; // vertical gap between blocks // State variables var stack = []; // Array of stacked blocks var currentBlock = null; // The moving block var currentLevel = 0; // How many blocks stacked var blockSpeed = BLOCK_SPEED_START; var blockDirection = 1; // 1 = right, -1 = left var isDropping = false; var isGameOver = false; var scoreTxt = null; var cameraY = 0; // Camera Y offset for following the stack // GUI: Score display scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Center score, but not in top left 100x100 scoreTxt.x = LK.gui.top.width / 2; scoreTxt.y = 30; // Add base block var baseBlock = new Block(); baseBlock.setBlockSize(BLOCK_START_WIDTH, BLOCK_HEIGHT); baseBlock.x = GAME_WIDTH / 2; baseBlock.y = BLOCK_Y_START; baseBlock.blockAsset.color = 0x21618c; // darker game.addChild(baseBlock); stack.push(baseBlock); // Start first moving block function spawnBlock() { var prevBlock = stack[stack.length - 1]; var newBlock = new Block(); newBlock.setBlockSize(prevBlock.blockAsset.width, BLOCK_HEIGHT); // Start at left or right edge, alternate var startX = blockDirection === 1 ? GAME_WIDTH / 2 - 400 : GAME_WIDTH / 2 + 400; newBlock.x = startX; newBlock.y = prevBlock.y - BLOCK_Y_GAP; game.addChild(newBlock); currentBlock = newBlock; isDropping = false; } function updateScore() { scoreTxt.setText(currentLevel); } // Drop the current block function dropBlock() { if (isDropping || isGameOver) return; isDropping = true; var prevBlock = stack[stack.length - 1]; var overlap = getOverlap(currentBlock, prevBlock); if (overlap.width <= 0 || overlap.width < BLOCK_MIN_WIDTH) { // No overlap or too small: game over currentBlock.animateDrop(prevBlock.y - BLOCK_Y_GAP, function () { endGame(); }); return; } // Animate block drop currentBlock.animateDrop(prevBlock.y - BLOCK_Y_GAP, function () { // Trim block to overlap var trimmedX = overlap.center; var trimmedWidth = overlap.width; // Check for perfect stack (allowing 5px tolerance) var isPerfect = Math.abs(currentBlock.x - prevBlock.x) < 5; // Calculate overhang areas var leftOverhang = 0; var rightOverhang = 0; var currentLeft = currentBlock.x - currentBlock.blockAsset.width / 2; var currentRight = currentBlock.x + currentBlock.blockAsset.width / 2; var prevLeft = prevBlock.x - prevBlock.blockAsset.width / 2; var prevRight = prevBlock.x + prevBlock.blockAsset.width / 2; // Calculate left overhang if (currentLeft < prevLeft) { leftOverhang = prevLeft - currentLeft; } // Calculate right overhang if (currentRight > prevRight) { rightOverhang = currentRight - prevRight; } // Animate trim currentBlock.animateTrim(trimmedWidth, trimmedX, function () { // Add left overhang if any if (leftOverhang > 0.5) { var leftOverhangX = prevLeft - leftOverhang / 2; var leftOverhangObj = new Overhang(); leftOverhangObj.setOverhangSize(leftOverhang, BLOCK_HEIGHT); leftOverhangObj.x = leftOverhangX; leftOverhangObj.y = currentBlock.y; leftOverhangObj.setColor(0xe74c3c); game.addChild(leftOverhangObj); leftOverhangObj.fall(function () { leftOverhangObj.destroy(); }); } // Add right overhang if any if (rightOverhang > 0.5) { var rightOverhangX = prevRight + rightOverhang / 2; var rightOverhangObj = new Overhang(); rightOverhangObj.setOverhangSize(rightOverhang, BLOCK_HEIGHT); rightOverhangObj.x = rightOverhangX; rightOverhangObj.y = currentBlock.y; rightOverhangObj.setColor(0xe74c3c); game.addChild(rightOverhangObj); rightOverhangObj.fall(function () { rightOverhangObj.destroy(); }); } // Show "Perfect" text if perfectly stacked if (isPerfect) { var perfectTxt = new Text2('Perfect!', { size: 120, fill: 0xFFD700 }); perfectTxt.anchor.set(0.5, 0.5); perfectTxt.x = currentBlock.x; perfectTxt.y = currentBlock.y - BLOCK_HEIGHT; game.addChild(perfectTxt); tween(perfectTxt, { y: perfectTxt.y - 120, alpha: 0 }, { duration: 700, easing: tween.cubicOut, onFinish: function onFinish() { perfectTxt.destroy(); } }); } // Play block placement sound LK.getSound('Blockkoyma').play(); // Finalize block currentBlock.setBlockSize(trimmedWidth, BLOCK_HEIGHT); currentBlock.x = trimmedX; stack.push(currentBlock); // Next round currentLevel += 1; updateScore(); // Increase speed a bit, with a cap to prevent it from becoming unplayable blockSpeed += BLOCK_SPEED_INCREMENT; if (blockSpeed > 40) blockSpeed = 40; // Cap speed for playability blockDirection *= -1; // alternate direction spawnBlock(); }); }); } // Calculate overlap between two blocks function getOverlap(blockA, blockB) { var leftA = blockA.x - blockA.blockAsset.width / 2; var rightA = blockA.x + blockA.blockAsset.width / 2; var leftB = blockB.x - blockB.blockAsset.width / 2; var rightB = blockB.x + blockB.blockAsset.width / 2; var left = Math.max(leftA, leftB); var right = Math.min(rightA, rightB); var width = right - left; var center = left + width / 2; return { width: width, center: center }; } // End game function endGame() { isGameOver = true; // Flash screen red LK.effects.flashScreen(0xe74c3c, 800); // Show game over popup (handled by LK) LK.showGameOver(); } // Handle tap/click to drop block game.down = function (x, y, obj) { if (isGameOver) return; dropBlock(); }; // Main update loop game.update = function () { if (isGameOver) return; if (!currentBlock) { spawnBlock(); return; } if (isDropping) return; // Move block horizontally currentBlock.x += blockSpeed * blockDirection; // Bounce at screen edges (keep inside 200px from edge) var minX = 200 + currentBlock.blockAsset.width / 2; var maxX = GAME_WIDTH - 200 - currentBlock.blockAsset.width / 2; if (currentBlock.x > maxX) { currentBlock.x = maxX; blockDirection = -1; } if (currentBlock.x < minX) { currentBlock.x = minX; blockDirection = 1; } // Update camera to follow the stack continuously up to 1000 points if (stack.length > 0 && currentLevel <= 1000) { var topBlock = stack[stack.length - 1]; // Include current moving block in camera calculation if it exists var highestY = topBlock.y; if (currentBlock && !isDropping) { highestY = Math.min(topBlock.y, currentBlock.y); } // Calculate target camera position to move camera down as stack grows up var targetCameraY = Math.max(0, BLOCK_Y_START - highestY - GAME_HEIGHT / 2); // Smooth camera movement cameraY += (targetCameraY - cameraY) * 0.15; // Reverse camera direction - positive cameraY moves view down game.y = cameraY; // Move background with camera at same speed background.y = -cameraY; // Move background at same speed as camera } }; // Reset game state on restart game.on('reset', function () { // Remove all blocks except base for (var i = 1; i < stack.length; i++) { stack[i].destroy(); } stack = [baseBlock]; baseBlock.setBlockSize(BLOCK_START_WIDTH, BLOCK_HEIGHT); baseBlock.x = GAME_WIDTH / 2; baseBlock.y = BLOCK_Y_START; currentBlock = null; currentLevel = 0; blockSpeed = BLOCK_SPEED_START; blockDirection = 1; isDropping = false; isGameOver = false; cameraY = 0; game.y = 0; updateScore(); }); // Initial score updateScore(); // Play background music LK.playMusic('StackMaster');
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Block class for stackable blocks
var Block = Container.expand(function () {
var self = Container.call(this);
// Attach block asset
var blockAsset = self.attachAsset('block', {
anchorX: 0.5,
anchorY: 0.5
});
// Store reference for resizing
self.blockAsset = blockAsset;
// Set initial width/height
self.setBlockSize = function (width, height) {
blockAsset.width = width;
blockAsset.height = height;
self.width = width;
self.height = height;
};
// Animate block to a new position (used for dropping)
self.animateDrop = function (targetY, onFinish) {
tween(self, {
y: targetY
}, {
duration: 180,
easing: tween.cubicIn,
onFinish: onFinish
});
};
// Animate block to a new width (used for trimming)
self.animateTrim = function (newWidth, newX, onFinish) {
tween(self.blockAsset, {
width: newWidth
}, {
duration: 120,
easing: tween.linear
});
tween(self, {
x: newX
}, {
duration: 120,
easing: tween.linear,
onFinish: onFinish
});
};
// Set color (for overhangs)
self.setColor = function (color) {
blockAsset.color = color;
};
return self;
});
// Overhang class for falling pieces
var Overhang = Container.expand(function () {
var self = Container.call(this);
var overhangAsset = self.attachAsset('overhang', {
anchorX: 0.5,
anchorY: 0.5
});
self.setOverhangSize = function (width, height) {
overhangAsset.width = width;
overhangAsset.height = height;
self.width = width;
self.height = height;
};
// Animate falling
self.fall = function (onFinish) {
tween(self, {
y: self.y + 400
}, {
duration: 400,
easing: tween.cubicIn,
onFinish: onFinish
});
tween(self, {
alpha: 0
}, {
duration: 400,
easing: tween.linear
});
};
// Set color
self.setColor = function (color) {
overhangAsset.color = color;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222a36
});
/****
* Game Code
****/
// Add background
var background = game.attachAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
// Scale background to cover full screen
background.width = GAME_WIDTH;
background.height = GAME_HEIGHT;
// Overhang asset: red
// Base block asset: darker blue
// Block asset: blue rectangle
// Game constants
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var BLOCK_HEIGHT = 100;
var BLOCK_START_WIDTH = 600;
var BLOCK_MIN_WIDTH = 40;
var BLOCK_SPEED_START = 12;
var BLOCK_SPEED_INCREMENT = 0.7;
var BLOCK_Y_START = GAME_HEIGHT - 400;
var BLOCK_Y_GAP = 90; // vertical gap between blocks
// State variables
var stack = []; // Array of stacked blocks
var currentBlock = null; // The moving block
var currentLevel = 0; // How many blocks stacked
var blockSpeed = BLOCK_SPEED_START;
var blockDirection = 1; // 1 = right, -1 = left
var isDropping = false;
var isGameOver = false;
var scoreTxt = null;
var cameraY = 0; // Camera Y offset for following the stack
// GUI: Score display
scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Center score, but not in top left 100x100
scoreTxt.x = LK.gui.top.width / 2;
scoreTxt.y = 30;
// Add base block
var baseBlock = new Block();
baseBlock.setBlockSize(BLOCK_START_WIDTH, BLOCK_HEIGHT);
baseBlock.x = GAME_WIDTH / 2;
baseBlock.y = BLOCK_Y_START;
baseBlock.blockAsset.color = 0x21618c; // darker
game.addChild(baseBlock);
stack.push(baseBlock);
// Start first moving block
function spawnBlock() {
var prevBlock = stack[stack.length - 1];
var newBlock = new Block();
newBlock.setBlockSize(prevBlock.blockAsset.width, BLOCK_HEIGHT);
// Start at left or right edge, alternate
var startX = blockDirection === 1 ? GAME_WIDTH / 2 - 400 : GAME_WIDTH / 2 + 400;
newBlock.x = startX;
newBlock.y = prevBlock.y - BLOCK_Y_GAP;
game.addChild(newBlock);
currentBlock = newBlock;
isDropping = false;
}
function updateScore() {
scoreTxt.setText(currentLevel);
}
// Drop the current block
function dropBlock() {
if (isDropping || isGameOver) return;
isDropping = true;
var prevBlock = stack[stack.length - 1];
var overlap = getOverlap(currentBlock, prevBlock);
if (overlap.width <= 0 || overlap.width < BLOCK_MIN_WIDTH) {
// No overlap or too small: game over
currentBlock.animateDrop(prevBlock.y - BLOCK_Y_GAP, function () {
endGame();
});
return;
}
// Animate block drop
currentBlock.animateDrop(prevBlock.y - BLOCK_Y_GAP, function () {
// Trim block to overlap
var trimmedX = overlap.center;
var trimmedWidth = overlap.width;
// Check for perfect stack (allowing 5px tolerance)
var isPerfect = Math.abs(currentBlock.x - prevBlock.x) < 5;
// Calculate overhang areas
var leftOverhang = 0;
var rightOverhang = 0;
var currentLeft = currentBlock.x - currentBlock.blockAsset.width / 2;
var currentRight = currentBlock.x + currentBlock.blockAsset.width / 2;
var prevLeft = prevBlock.x - prevBlock.blockAsset.width / 2;
var prevRight = prevBlock.x + prevBlock.blockAsset.width / 2;
// Calculate left overhang
if (currentLeft < prevLeft) {
leftOverhang = prevLeft - currentLeft;
}
// Calculate right overhang
if (currentRight > prevRight) {
rightOverhang = currentRight - prevRight;
}
// Animate trim
currentBlock.animateTrim(trimmedWidth, trimmedX, function () {
// Add left overhang if any
if (leftOverhang > 0.5) {
var leftOverhangX = prevLeft - leftOverhang / 2;
var leftOverhangObj = new Overhang();
leftOverhangObj.setOverhangSize(leftOverhang, BLOCK_HEIGHT);
leftOverhangObj.x = leftOverhangX;
leftOverhangObj.y = currentBlock.y;
leftOverhangObj.setColor(0xe74c3c);
game.addChild(leftOverhangObj);
leftOverhangObj.fall(function () {
leftOverhangObj.destroy();
});
}
// Add right overhang if any
if (rightOverhang > 0.5) {
var rightOverhangX = prevRight + rightOverhang / 2;
var rightOverhangObj = new Overhang();
rightOverhangObj.setOverhangSize(rightOverhang, BLOCK_HEIGHT);
rightOverhangObj.x = rightOverhangX;
rightOverhangObj.y = currentBlock.y;
rightOverhangObj.setColor(0xe74c3c);
game.addChild(rightOverhangObj);
rightOverhangObj.fall(function () {
rightOverhangObj.destroy();
});
}
// Show "Perfect" text if perfectly stacked
if (isPerfect) {
var perfectTxt = new Text2('Perfect!', {
size: 120,
fill: 0xFFD700
});
perfectTxt.anchor.set(0.5, 0.5);
perfectTxt.x = currentBlock.x;
perfectTxt.y = currentBlock.y - BLOCK_HEIGHT;
game.addChild(perfectTxt);
tween(perfectTxt, {
y: perfectTxt.y - 120,
alpha: 0
}, {
duration: 700,
easing: tween.cubicOut,
onFinish: function onFinish() {
perfectTxt.destroy();
}
});
}
// Play block placement sound
LK.getSound('Blockkoyma').play();
// Finalize block
currentBlock.setBlockSize(trimmedWidth, BLOCK_HEIGHT);
currentBlock.x = trimmedX;
stack.push(currentBlock);
// Next round
currentLevel += 1;
updateScore();
// Increase speed a bit, with a cap to prevent it from becoming unplayable
blockSpeed += BLOCK_SPEED_INCREMENT;
if (blockSpeed > 40) blockSpeed = 40; // Cap speed for playability
blockDirection *= -1; // alternate direction
spawnBlock();
});
});
}
// Calculate overlap between two blocks
function getOverlap(blockA, blockB) {
var leftA = blockA.x - blockA.blockAsset.width / 2;
var rightA = blockA.x + blockA.blockAsset.width / 2;
var leftB = blockB.x - blockB.blockAsset.width / 2;
var rightB = blockB.x + blockB.blockAsset.width / 2;
var left = Math.max(leftA, leftB);
var right = Math.min(rightA, rightB);
var width = right - left;
var center = left + width / 2;
return {
width: width,
center: center
};
}
// End game
function endGame() {
isGameOver = true;
// Flash screen red
LK.effects.flashScreen(0xe74c3c, 800);
// Show game over popup (handled by LK)
LK.showGameOver();
}
// Handle tap/click to drop block
game.down = function (x, y, obj) {
if (isGameOver) return;
dropBlock();
};
// Main update loop
game.update = function () {
if (isGameOver) return;
if (!currentBlock) {
spawnBlock();
return;
}
if (isDropping) return;
// Move block horizontally
currentBlock.x += blockSpeed * blockDirection;
// Bounce at screen edges (keep inside 200px from edge)
var minX = 200 + currentBlock.blockAsset.width / 2;
var maxX = GAME_WIDTH - 200 - currentBlock.blockAsset.width / 2;
if (currentBlock.x > maxX) {
currentBlock.x = maxX;
blockDirection = -1;
}
if (currentBlock.x < minX) {
currentBlock.x = minX;
blockDirection = 1;
}
// Update camera to follow the stack continuously up to 1000 points
if (stack.length > 0 && currentLevel <= 1000) {
var topBlock = stack[stack.length - 1];
// Include current moving block in camera calculation if it exists
var highestY = topBlock.y;
if (currentBlock && !isDropping) {
highestY = Math.min(topBlock.y, currentBlock.y);
}
// Calculate target camera position to move camera down as stack grows up
var targetCameraY = Math.max(0, BLOCK_Y_START - highestY - GAME_HEIGHT / 2);
// Smooth camera movement
cameraY += (targetCameraY - cameraY) * 0.15;
// Reverse camera direction - positive cameraY moves view down
game.y = cameraY;
// Move background with camera at same speed
background.y = -cameraY; // Move background at same speed as camera
}
};
// Reset game state on restart
game.on('reset', function () {
// Remove all blocks except base
for (var i = 1; i < stack.length; i++) {
stack[i].destroy();
}
stack = [baseBlock];
baseBlock.setBlockSize(BLOCK_START_WIDTH, BLOCK_HEIGHT);
baseBlock.x = GAME_WIDTH / 2;
baseBlock.y = BLOCK_Y_START;
currentBlock = null;
currentLevel = 0;
blockSpeed = BLOCK_SPEED_START;
blockDirection = 1;
isDropping = false;
isGameOver = false;
cameraY = 0;
game.y = 0;
updateScore();
});
// Initial score
updateScore();
// Play background music
LK.playMusic('StackMaster');