/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Ball class var Ball = Container.expand(function () { var self = Container.call(this); var ballGfx = self.attachAsset('ball', { anchorX: 0.5, anchorY: 0.5 }); self.radius = ballGfx.width / 2; self.vx = 0; self.vy = 0; self.speed = 22; self.stuck = true; // Ball stuck to paddle at start self.update = function () { if (self.stuck) { // Ball follows paddle self.x = paddle.x; self.y = paddle.y - paddle.height / 2 - self.radius; return; } self.x += self.vx; self.y += self.vy; }; return self; }); // Block class var Block = Container.expand(function () { var self = Container.call(this); self.type = 1; // 1: normal, 2: strong, 3: indestructible self.hits = 1; self.destroyed = false; self.powerup = false; self.setType = function (type) { self.type = type; if (type === 1) { self.attachAsset('block1', { anchorX: 0.5, anchorY: 0.5 }); self.hits = 1; } else if (type === 2) { self.attachAsset('block2', { anchorX: 0.5, anchorY: 0.5 }); self.hits = 2; } else if (type === 3) { self.attachAsset('blockInd', { anchorX: 0.5, anchorY: 0.5 }); self.hits = 9999; } }; return self; }); // HeartCircle class for animated heart lives var HeartCircle = Container.expand(function () { var self = Container.call(this); var heartGfx = self.attachAsset('heartCircle', { anchorX: 0.5, anchorY: 0.5 }); self.radius = 60; self.angle = 0; self.setPosition = function (cx, cy, r, angle) { self.x = cx + r * Math.cos(angle); self.y = cy + r * Math.sin(angle); }; self.setScale = function (scale) { self.scaleX = self.scaleY = scale; }; return self; }); // Paddle class var Paddle = Container.expand(function () { var self = Container.call(this); var paddleGfx = self.attachAsset('paddle', { anchorX: 0.5, anchorY: 0.5 }); self.width = paddleGfx.width; self.height = paddleGfx.height; self.extended = false; self.extensionTimeout = null; self.extend = function () { if (self.extended) return; self.extended = true; tween(self, { scaleX: 1.7 }, { duration: 300, easing: tween.easeOut }); if (self.extensionTimeout) LK.clearTimeout(self.extensionTimeout); self.extensionTimeout = LK.setTimeout(function () { tween(self, { scaleX: 1 }, { duration: 300, easing: tween.easeIn }); self.extended = false; }, 7000); }; return self; }); // Powerup class var Powerup = Container.expand(function () { var self = Container.call(this); var gfx = self.attachAsset('powerup', { anchorX: 0.5, anchorY: 0.5 }); // 10 powerup types var types = ['extend', // 0: Extend paddle 'shrink', // 1: Shrink paddle 'multi', // 2: Multi-ball 'slow', // 3: Slow ball 'fast', // 4: Fast ball 'catch', // 5: Sticky paddle 'life', // 6: Extra life 'bigball', // 7: Big ball 'smallball', // 8: Small ball 'score' // 9: Bonus score ]; // Randomly assign type if not set self.type = types[Math.floor(Math.random() * types.length)]; self.vy = 10; self.update = function () { self.y += self.vy; }; // Set color based on type for visual feedback var colorMap = { 'extend': 0x44ff44, 'shrink': 0xff4444, 'multi': 0x44aaff, 'slow': 0x8888ff, 'fast': 0xffaa00, 'catch': 0xff00ff, 'life': 0xffe066, 'bigball': 0x00ffff, 'smallball': 0x888888, 'score': 0xffffff }; if (gfx) gfx.tint = colorMap[self.type] || 0x44ff44; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181830 }); /**** * Game Code ****/ // background asset // Game constants // Ball: white circle // Paddle: blue rectangle // Block: colored rectangle (normal) // Block: strong (2 hits) // Block: indestructible // Powerup: green circle var GAME_W = 2048; var GAME_H = 2732; // Add background image to the game scene var background = LK.getAsset('background', { anchorX: 0.5, anchorY: 0.5, x: GAME_W / 2, y: GAME_H / 2, scaleX: GAME_W / LK.getAsset('background', {}).width, scaleY: GAME_H / LK.getAsset('background', {}).height, alpha: 1 }); game.addChildAt(background, 0); var BLOCK_ROWS = 4; var BLOCK_COLS = 6; var BLOCK_MARGIN_X = 8; var BLOCK_MARGIN_Y = 24; var BLOCK_START_Y = 320; var LIVES_START = 3; // Game state var paddle, ball; var blocks = []; var powerups = []; var lives = LIVES_START; var score = 0; var balls = []; var isLaunching = false; var lastTouchX = GAME_W / 2; var gameOver = false; // GUI var scoreTxt = new Text2('0', { size: 100, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Level text for displaying current level (used in level transitions) var levelText = null; // Heart circle lives var heartCircles = []; var heartCircleCenter = { x: GAME_W - 220, y: 120 }; var heartCircleRadius = 90; var heartCircleScale = 1.0; // Helper: update GUI function updateScore() { scoreTxt.setText(score); } function updateLives() { // Remove old hearts for (var i = 0; i < heartCircles.length; ++i) { heartCircles[i].destroy(); } heartCircles = []; // Place hearts in a circle, one after another var total = lives; var baseAngle = -Math.PI / 2; for (var i = 0; i < total; ++i) { var hc = new HeartCircle(); var angle = baseAngle + i * (Math.PI * 2) / Math.max(3, total); hc.setPosition(heartCircleCenter.x, heartCircleCenter.y, heartCircleRadius, angle); hc.setScale(1.0); LK.gui.topRight.addChild(hc); heartCircles.push(hc); } } // Helper: reset game state function resetGame() { // Remove old blocks for (var i = 0; i < blocks.length; ++i) blocks[i].destroy(); blocks = []; // Remove old balls for (var i = 0; i < balls.length; ++i) balls[i].destroy(); balls = []; // Remove powerups for (var i = 0; i < powerups.length; ++i) powerups[i].destroy(); powerups = []; // Reset score/lives score = 0; lives = LIVES_START; updateScore(); updateLives(); gameOver = false; // Create blocks var blockW = LK.getAsset('block1', {}).width; var blockH = LK.getAsset('block1', {}).height; var blockIndW = LK.getAsset('blockInd', {}).width; var blockIndH = LK.getAsset('blockInd', {}).height; var totalW = BLOCK_COLS * blockW + (BLOCK_COLS - 1) * BLOCK_MARGIN_X; var startX = (GAME_W - totalW) / 2 + blockW / 2; for (var row = 0; row < BLOCK_ROWS; ++row) { for (var col = 0; col < BLOCK_COLS; ++col) { var b = new Block(); // Use both block1 and block2 in every level, and make each level different // Example: alternate block1/block2, and shift pattern by level for variety var levelPattern = typeof currentLevel !== "undefined" ? currentLevel : 1; if ((row + col + levelPattern) % 2 === 0) { b.setType(1); } else { b.setType(2); } // Always use normal block size/position b.x = startX + col * (blockW + BLOCK_MARGIN_X); b.y = BLOCK_START_Y + row * (blockH + BLOCK_MARGIN_Y); b.width = blockW; b.height = blockH; // 15% chance to drop powerup if (Math.random() < 0.15) { // Assign a random type to the block's powerup property var powerupTypes = ['extend', 'shrink', 'multi', 'slow', 'fast', 'catch', 'life', 'bigball', 'smallball', 'score']; b.powerup = powerupTypes[Math.floor(Math.random() * powerupTypes.length)]; } blocks.push(b); game.addChild(b); } } // Create paddle if (paddle) paddle.destroy(); paddle = new Paddle(); paddle.x = GAME_W / 2; paddle.y = GAME_H - 220; game.addChild(paddle); // Create ball spawnBall(); } // Helper: spawn a new ball (stuck to paddle) function spawnBall() { var b = new Ball(); b.x = paddle.x; b.y = paddle.y - paddle.height / 2 - b.radius; b.stuck = true; b.vx = 0; b.vy = 0; balls.push(b); game.addChild(b); ball = b; } // Helper: launch ball function launchBall() { if (!ball || !ball.stuck) return; var angle = (Math.random() * 0.5 + 0.25) * Math.PI; // 45-135 deg ball.vx = ball.speed * Math.cos(angle); ball.vy = -ball.speed * Math.abs(Math.sin(angle)); ball.stuck = false; } // Helper: check collision (AABB) function rectsIntersect(a, b) { return a.x - a.width / 2 < b.x + b.width / 2 && a.x + a.width / 2 > b.x - b.width / 2 && a.y - a.height / 2 < b.y + b.height / 2 && a.y + a.height / 2 > b.y - b.height / 2; } // Helper: check ball-block collision (circle-rect) function ballBlockCollision(ball, block) { var bx = block.x, by = block.y, bw = block.width, bh = block.height; var cx = ball.x, cy = ball.y, r = ball.radius; // Clamp point var px = Math.max(bx - bw / 2, Math.min(cx, bx + bw / 2)); var py = Math.max(by - bh / 2, Math.min(cy, by + bh / 2)); var dx = cx - px, dy = cy - py; return dx * dx + dy * dy < r * r; } // Helper: reflect ball on block function reflectBall(ball, block) { // Find side of collision var overlapX = block.x - block.width / 2 - (ball.x + ball.radius); var overlapY = block.y - block.height / 2 - (ball.y + ball.radius); var prevX = ball.x - ball.vx, prevY = ball.y - ball.vy; // Check which axis ball came from var fromLeft = prevX < block.x - block.width / 2; var fromRight = prevX > block.x + block.width / 2; var fromTop = prevY < block.y - block.height / 2; var fromBottom = prevY > block.y + block.height / 2; // Simple: invert vy if hit top/bottom, vx if hit left/right if (Math.abs(ball.vx) > Math.abs(ball.vy)) { ball.vx *= -1; } else { ball.vy *= -1; } } // Helper: reflect ball on paddle function reflectBallPaddle(ball, paddle) { // Calculate hit position relative to paddle center (-1 to 1) var rel = (ball.x - paddle.x) / (paddle.width * (paddle.scaleX || 1) / 2); rel = Math.max(-1, Math.min(1, rel)); var angle = rel * Math.PI / 3 + Math.PI / 2; // -60deg to +60deg from vertical var speed = ball.speed; ball.vx = speed * Math.sin(angle); ball.vy = -Math.abs(speed * Math.cos(angle)); } // Helper: spawn powerup function spawnPowerup(x, y, type) { var p = new Powerup(); p.x = x; p.y = y; if (type) p.type = type; powerups.push(p); game.addChild(p); } // Game move handler (drag paddle) game.move = function (x, y, obj) { if (gameOver) return; // Clamp paddle to screen var px = Math.max(paddle.width / 2, Math.min(GAME_W - paddle.width / 2, x)); paddle.x = px; lastTouchX = px; // If ball is stuck, move it too for (var i = 0; i < balls.length; ++i) { if (balls[i].stuck) balls[i].x = px; } }; // Game down handler (launch ball) game.down = function (x, y, obj) { if (gameOver) return; if (ball && ball.stuck) { launchBall(); } }; // Main game update game.update = function () { if (gameOver) return; // Update balls for (var i = balls.length - 1; i >= 0; --i) { var b = balls[i]; b.update(); // Wall collisions if (b.x - b.radius < 0) { b.x = b.radius; b.vx = Math.abs(b.vx); } if (b.x + b.radius > GAME_W) { b.x = GAME_W - b.radius; b.vx = -Math.abs(b.vx); } if (b.y - b.radius < 0) { b.y = b.radius; b.vy = Math.abs(b.vy); } // Paddle collision if (!b.stuck && rectsIntersect(b, paddle)) { reflectBallPaddle(b, paddle); // Nudge ball out of paddle b.y = paddle.y - paddle.height / 2 - b.radius - 2; } // Block collisions for (var j = blocks.length - 1; j >= 0; --j) { var block = blocks[j]; if (block.destroyed) continue; if (ballBlockCollision(b, block)) { // Hit! if (block.type !== 3) { block.hits -= 1; if (block.hits <= 0) { block.destroyed = true; tween(block, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { block.destroy(); } }); blocks.splice(j, 1); score += 100; updateScore(); // Powerup drop if (block.powerup) { spawnPowerup(block.x, block.y, block.powerup); } // Win condition var anyLeft = false; for (var k = 0; k < blocks.length; ++k) { if (blocks[k].type !== 3) { anyLeft = true; break; } } if (!anyLeft) { // Animate all remaining blocks out (milk animation) for (var m = 0; m < blocks.length; ++m) { var milkBlock = blocks[m]; tween(milkBlock, { scaleY: 0.1, alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function (blk) { return function () { blk.destroy(); }; }(milkBlock) }); } blocks = []; // After animation, spawn a new wave of blocks for the next level LK.setTimeout(function () { // Level system: 30 levels, each with unique block patterns if (typeof currentLevel === "undefined") { currentLevel = 1; } else { currentLevel += 1; } if (currentLevel > 30) currentLevel = 30; // Cap at 30 // Helper: get block type for a given level, row, col function getBlockTypeForLevel(level, row, col) { // Level 1-5: simple, more normal, some strong, rare indestructible if (level <= 5) { if (row === 0 && col % 2 === 0) return 2; if (row === 1 && col % 3 === 0) return 2; if (row === 2 && col % 5 === 0) return 3; return 1; } // Level 6-10: more strong, some indestructible if (level <= 10) { if ((row + col) % 4 === 0) return 3; if ((row + col) % 2 === 0) return 2; return 1; } // Level 11-15: checkerboard indestructible, strong if (level <= 15) { if ((row + col) % 2 === 0) return 3; if (row * col % 3 === 0) return 2; return 1; } // Level 16-20: border indestructible, center strong if (level <= 20) { if (row === 0 || row === BLOCK_ROWS - 1 || col === 0 || col === BLOCK_COLS - 1) return 3; if ((row + col) % 2 === 1) return 2; return 1; } // Level 21-25: stripes of indestructible, strong if (level <= 25) { if (col % 2 === 0) return 3; if (row % 2 === 0) return 2; return 1; } // Level 26-30: lots of indestructible, strong in center if (level <= 30) { if ((row === 1 || row === BLOCK_ROWS - 2) && (col === 2 || col === BLOCK_COLS - 3)) return 2; if ((row + col) % 2 === 0) return 3; return 1; } return 1; } // Helper: get powerup chance for a given level function getPowerupChanceForLevel(level) { if (level <= 5) return 0.20; if (level <= 10) return 0.18; if (level <= 15) return 0.16; if (level <= 20) return 0.14; if (level <= 25) return 0.12; return 0.10; } // Show level number (optional: can be removed if not wanted) if (!levelText) { levelText = new Text2('Level ' + currentLevel, { size: 120, fill: "#fff" }); levelText.anchor.set(0.5, 0.5); LK.gui.center.addChild(levelText); } else { levelText.setText('Level ' + currentLevel); levelText.visible = true; } // Hide after 1s LK.setTimeout(function () { if (levelText) levelText.visible = false; }, 1000); // Block placement var blockW = LK.getAsset('block1', {}).width; var blockH = LK.getAsset('block1', {}).height; var blockIndW = LK.getAsset('blockInd', {}).width; var blockIndH = LK.getAsset('blockInd', {}).height; var totalW = BLOCK_COLS * blockW + (BLOCK_COLS - 1) * BLOCK_MARGIN_X; var startX = (GAME_W - totalW) / 2 + blockW / 2; for (var row = 0; row < BLOCK_ROWS; ++row) { for (var col = 0; col < BLOCK_COLS; ++col) { var b = new Block(); // Use both block1 and block2 in every level, and make each level different // Example: alternate block1/block2, and shift pattern by level for variety if ((row + col + currentLevel) % 2 === 0) { b.setType(1); } else { b.setType(2); } // Always use normal block size/position b.x = startX + col * (blockW + BLOCK_MARGIN_X); b.y = BLOCK_START_Y + row * (blockH + BLOCK_MARGIN_Y); b.width = blockW; b.height = blockH; // Powerup chance by level var powerupChance = getPowerupChanceForLevel(currentLevel); if (Math.random() < powerupChance) b.powerup = true; blocks.push(b); game.addChild(b); // Animate in (milk drop) b.scaleY = 0.1; b.alpha = 0; tween(b, { scaleY: 1, alpha: 1 }, { duration: 500, easing: tween.easeOut }); } } }, 550); // Wait for milk animation out return; } } } // Flash block LK.effects.flashObject(block, 0xffffff, 100); // Reflect ball reflectBall(b, block); break; } } // Ball falls below paddle if (b.y - b.radius > GAME_H) { b.destroy(); balls.splice(i, 1); if (balls.length === 0) { lives -= 1; updateLives(); if (lives <= 0) { gameOver = true; LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); return; } else { spawnBall(); } } } } // Update powerups for (var i = powerups.length - 1; i >= 0; --i) { var p = powerups[i]; p.update(); // Paddle catch if (rectsIntersect(p, paddle)) { // Powerup effect logic if (p.type === 'extend') { paddle.extend(); } else if (p.type === 'shrink') { // Shrink paddle for 7s if (!paddle.shrunk) { paddle.shrunk = true; tween(paddle, { scaleX: 0.6 }, { duration: 300, easing: tween.easeOut }); if (paddle.shrinkTimeout) LK.clearTimeout(paddle.shrinkTimeout); paddle.shrinkTimeout = LK.setTimeout(function () { tween(paddle, { scaleX: 1 }, { duration: 300, easing: tween.easeIn }); paddle.shrunk = false; }, 7000); } } else if (p.type === 'multi') { // Multi-ball: spawn 2 extra balls for (var mb = 0; mb < 2; ++mb) { var newBall = new Ball(); newBall.x = ball.x; newBall.y = ball.y; newBall.stuck = false; var angle = (Math.random() * 0.5 + 0.25) * Math.PI; newBall.vx = ball.speed * Math.cos(angle) * (Math.random() < 0.5 ? 1 : -1); newBall.vy = -ball.speed * Math.abs(Math.sin(angle)); balls.push(newBall); game.addChild(newBall); } } else if (p.type === 'slow') { // Slow all balls for 7s for (var sb = 0; sb < balls.length; ++sb) { var b = balls[sb]; if (!b.slowed) { b.slowed = true; b.speed = 12; var mag = Math.sqrt(b.vx * b.vx + b.vy * b.vy); if (mag > 0) { b.vx *= 12 / mag; b.vy *= 12 / mag; } } } if (!game.slowTimeout) { game.slowTimeout = LK.setTimeout(function () { for (var sb = 0; sb < balls.length; ++sb) { var b = balls[sb]; if (b.slowed) { b.speed = 22; b.slowed = false; var mag = Math.sqrt(b.vx * b.vx + b.vy * b.vy); if (mag > 0) { b.vx *= 22 / mag; b.vy *= 22 / mag; } } } game.slowTimeout = null; }, 7000); } } else if (p.type === 'fast') { // Speed up all balls for 7s for (var fb = 0; fb < balls.length; ++fb) { var b = balls[fb]; if (!b.fasted) { b.fasted = true; b.speed = 36; var mag = Math.sqrt(b.vx * b.vx + b.vy * b.vy); if (mag > 0) { b.vx *= 36 / mag; b.vy *= 36 / mag; } } } if (!game.fastTimeout) { game.fastTimeout = LK.setTimeout(function () { for (var fb = 0; fb < balls.length; ++fb) { var b = balls[fb]; if (b.fasted) { b.speed = 22; b.fasted = false; var mag = Math.sqrt(b.vx * b.vx + b.vy * b.vy); if (mag > 0) { b.vx *= 22 / mag; b.vy *= 22 / mag; } } } game.fastTimeout = null; }, 7000); } } else if (p.type === 'catch') { // Sticky paddle for 7s paddle.sticky = true; if (paddle.stickyTimeout) LK.clearTimeout(paddle.stickyTimeout); paddle.stickyTimeout = LK.setTimeout(function () { paddle.sticky = false; }, 7000); } else if (p.type === 'life') { // Extra life lives += 1; updateLives(); } else if (p.type === 'bigball') { // Make all balls big for 7s for (var bb = 0; bb < balls.length; ++bb) { var b = balls[bb]; if (!b.bigged) { b.bigged = true; b.scaleX = b.scaleY = 1.5; b.radius = LK.getAsset('ball', {}).width * 0.75; } } if (!game.bigTimeout) { game.bigTimeout = LK.setTimeout(function () { for (var bb = 0; bb < balls.length; ++bb) { var b = balls[bb]; if (b.bigged) { b.scaleX = b.scaleY = 1; b.radius = LK.getAsset('ball', {}).width / 2; b.bigged = false; } } game.bigTimeout = null; }, 7000); } } else if (p.type === 'smallball') { // Make all balls small for 7s for (var sb = 0; sb < balls.length; ++sb) { var b = balls[sb]; if (!b.smalled) { b.smalled = true; b.scaleX = b.scaleY = 0.6; b.radius = LK.getAsset('ball', {}).width * 0.3; } } if (!game.smallTimeout) { game.smallTimeout = LK.setTimeout(function () { for (var sb = 0; sb < balls.length; ++sb) { var b = balls[sb]; if (b.smalled) { b.scaleX = b.scaleY = 1; b.radius = LK.getAsset('ball', {}).width / 2; b.smalled = false; } } game.smallTimeout = null; }, 7000); } } else if (p.type === 'score') { // Bonus score score += 500; updateScore(); } p.destroy(); powerups.splice(i, 1); } else if (p.y - p.height / 2 > GAME_H) { p.destroy(); powerups.splice(i, 1); } } }; // Start game resetGame();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball class
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballGfx = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = ballGfx.width / 2;
self.vx = 0;
self.vy = 0;
self.speed = 22;
self.stuck = true; // Ball stuck to paddle at start
self.update = function () {
if (self.stuck) {
// Ball follows paddle
self.x = paddle.x;
self.y = paddle.y - paddle.height / 2 - self.radius;
return;
}
self.x += self.vx;
self.y += self.vy;
};
return self;
});
// Block class
var Block = Container.expand(function () {
var self = Container.call(this);
self.type = 1; // 1: normal, 2: strong, 3: indestructible
self.hits = 1;
self.destroyed = false;
self.powerup = false;
self.setType = function (type) {
self.type = type;
if (type === 1) {
self.attachAsset('block1', {
anchorX: 0.5,
anchorY: 0.5
});
self.hits = 1;
} else if (type === 2) {
self.attachAsset('block2', {
anchorX: 0.5,
anchorY: 0.5
});
self.hits = 2;
} else if (type === 3) {
self.attachAsset('blockInd', {
anchorX: 0.5,
anchorY: 0.5
});
self.hits = 9999;
}
};
return self;
});
// HeartCircle class for animated heart lives
var HeartCircle = Container.expand(function () {
var self = Container.call(this);
var heartGfx = self.attachAsset('heartCircle', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 60;
self.angle = 0;
self.setPosition = function (cx, cy, r, angle) {
self.x = cx + r * Math.cos(angle);
self.y = cy + r * Math.sin(angle);
};
self.setScale = function (scale) {
self.scaleX = self.scaleY = scale;
};
return self;
});
// Paddle class
var Paddle = Container.expand(function () {
var self = Container.call(this);
var paddleGfx = self.attachAsset('paddle', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = paddleGfx.width;
self.height = paddleGfx.height;
self.extended = false;
self.extensionTimeout = null;
self.extend = function () {
if (self.extended) return;
self.extended = true;
tween(self, {
scaleX: 1.7
}, {
duration: 300,
easing: tween.easeOut
});
if (self.extensionTimeout) LK.clearTimeout(self.extensionTimeout);
self.extensionTimeout = LK.setTimeout(function () {
tween(self, {
scaleX: 1
}, {
duration: 300,
easing: tween.easeIn
});
self.extended = false;
}, 7000);
};
return self;
});
// Powerup class
var Powerup = Container.expand(function () {
var self = Container.call(this);
var gfx = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
// 10 powerup types
var types = ['extend',
// 0: Extend paddle
'shrink',
// 1: Shrink paddle
'multi',
// 2: Multi-ball
'slow',
// 3: Slow ball
'fast',
// 4: Fast ball
'catch',
// 5: Sticky paddle
'life',
// 6: Extra life
'bigball',
// 7: Big ball
'smallball',
// 8: Small ball
'score' // 9: Bonus score
];
// Randomly assign type if not set
self.type = types[Math.floor(Math.random() * types.length)];
self.vy = 10;
self.update = function () {
self.y += self.vy;
};
// Set color based on type for visual feedback
var colorMap = {
'extend': 0x44ff44,
'shrink': 0xff4444,
'multi': 0x44aaff,
'slow': 0x8888ff,
'fast': 0xffaa00,
'catch': 0xff00ff,
'life': 0xffe066,
'bigball': 0x00ffff,
'smallball': 0x888888,
'score': 0xffffff
};
if (gfx) gfx.tint = colorMap[self.type] || 0x44ff44;
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181830
});
/****
* Game Code
****/
// background asset
// Game constants
// Ball: white circle
// Paddle: blue rectangle
// Block: colored rectangle (normal)
// Block: strong (2 hits)
// Block: indestructible
// Powerup: green circle
var GAME_W = 2048;
var GAME_H = 2732;
// Add background image to the game scene
var background = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_W / 2,
y: GAME_H / 2,
scaleX: GAME_W / LK.getAsset('background', {}).width,
scaleY: GAME_H / LK.getAsset('background', {}).height,
alpha: 1
});
game.addChildAt(background, 0);
var BLOCK_ROWS = 4;
var BLOCK_COLS = 6;
var BLOCK_MARGIN_X = 8;
var BLOCK_MARGIN_Y = 24;
var BLOCK_START_Y = 320;
var LIVES_START = 3;
// Game state
var paddle, ball;
var blocks = [];
var powerups = [];
var lives = LIVES_START;
var score = 0;
var balls = [];
var isLaunching = false;
var lastTouchX = GAME_W / 2;
var gameOver = false;
// GUI
var scoreTxt = new Text2('0', {
size: 100,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Level text for displaying current level (used in level transitions)
var levelText = null;
// Heart circle lives
var heartCircles = [];
var heartCircleCenter = {
x: GAME_W - 220,
y: 120
};
var heartCircleRadius = 90;
var heartCircleScale = 1.0;
// Helper: update GUI
function updateScore() {
scoreTxt.setText(score);
}
function updateLives() {
// Remove old hearts
for (var i = 0; i < heartCircles.length; ++i) {
heartCircles[i].destroy();
}
heartCircles = [];
// Place hearts in a circle, one after another
var total = lives;
var baseAngle = -Math.PI / 2;
for (var i = 0; i < total; ++i) {
var hc = new HeartCircle();
var angle = baseAngle + i * (Math.PI * 2) / Math.max(3, total);
hc.setPosition(heartCircleCenter.x, heartCircleCenter.y, heartCircleRadius, angle);
hc.setScale(1.0);
LK.gui.topRight.addChild(hc);
heartCircles.push(hc);
}
}
// Helper: reset game state
function resetGame() {
// Remove old blocks
for (var i = 0; i < blocks.length; ++i) blocks[i].destroy();
blocks = [];
// Remove old balls
for (var i = 0; i < balls.length; ++i) balls[i].destroy();
balls = [];
// Remove powerups
for (var i = 0; i < powerups.length; ++i) powerups[i].destroy();
powerups = [];
// Reset score/lives
score = 0;
lives = LIVES_START;
updateScore();
updateLives();
gameOver = false;
// Create blocks
var blockW = LK.getAsset('block1', {}).width;
var blockH = LK.getAsset('block1', {}).height;
var blockIndW = LK.getAsset('blockInd', {}).width;
var blockIndH = LK.getAsset('blockInd', {}).height;
var totalW = BLOCK_COLS * blockW + (BLOCK_COLS - 1) * BLOCK_MARGIN_X;
var startX = (GAME_W - totalW) / 2 + blockW / 2;
for (var row = 0; row < BLOCK_ROWS; ++row) {
for (var col = 0; col < BLOCK_COLS; ++col) {
var b = new Block();
// Use both block1 and block2 in every level, and make each level different
// Example: alternate block1/block2, and shift pattern by level for variety
var levelPattern = typeof currentLevel !== "undefined" ? currentLevel : 1;
if ((row + col + levelPattern) % 2 === 0) {
b.setType(1);
} else {
b.setType(2);
}
// Always use normal block size/position
b.x = startX + col * (blockW + BLOCK_MARGIN_X);
b.y = BLOCK_START_Y + row * (blockH + BLOCK_MARGIN_Y);
b.width = blockW;
b.height = blockH;
// 15% chance to drop powerup
if (Math.random() < 0.15) {
// Assign a random type to the block's powerup property
var powerupTypes = ['extend', 'shrink', 'multi', 'slow', 'fast', 'catch', 'life', 'bigball', 'smallball', 'score'];
b.powerup = powerupTypes[Math.floor(Math.random() * powerupTypes.length)];
}
blocks.push(b);
game.addChild(b);
}
}
// Create paddle
if (paddle) paddle.destroy();
paddle = new Paddle();
paddle.x = GAME_W / 2;
paddle.y = GAME_H - 220;
game.addChild(paddle);
// Create ball
spawnBall();
}
// Helper: spawn a new ball (stuck to paddle)
function spawnBall() {
var b = new Ball();
b.x = paddle.x;
b.y = paddle.y - paddle.height / 2 - b.radius;
b.stuck = true;
b.vx = 0;
b.vy = 0;
balls.push(b);
game.addChild(b);
ball = b;
}
// Helper: launch ball
function launchBall() {
if (!ball || !ball.stuck) return;
var angle = (Math.random() * 0.5 + 0.25) * Math.PI; // 45-135 deg
ball.vx = ball.speed * Math.cos(angle);
ball.vy = -ball.speed * Math.abs(Math.sin(angle));
ball.stuck = false;
}
// Helper: check collision (AABB)
function rectsIntersect(a, b) {
return a.x - a.width / 2 < b.x + b.width / 2 && a.x + a.width / 2 > b.x - b.width / 2 && a.y - a.height / 2 < b.y + b.height / 2 && a.y + a.height / 2 > b.y - b.height / 2;
}
// Helper: check ball-block collision (circle-rect)
function ballBlockCollision(ball, block) {
var bx = block.x,
by = block.y,
bw = block.width,
bh = block.height;
var cx = ball.x,
cy = ball.y,
r = ball.radius;
// Clamp point
var px = Math.max(bx - bw / 2, Math.min(cx, bx + bw / 2));
var py = Math.max(by - bh / 2, Math.min(cy, by + bh / 2));
var dx = cx - px,
dy = cy - py;
return dx * dx + dy * dy < r * r;
}
// Helper: reflect ball on block
function reflectBall(ball, block) {
// Find side of collision
var overlapX = block.x - block.width / 2 - (ball.x + ball.radius);
var overlapY = block.y - block.height / 2 - (ball.y + ball.radius);
var prevX = ball.x - ball.vx,
prevY = ball.y - ball.vy;
// Check which axis ball came from
var fromLeft = prevX < block.x - block.width / 2;
var fromRight = prevX > block.x + block.width / 2;
var fromTop = prevY < block.y - block.height / 2;
var fromBottom = prevY > block.y + block.height / 2;
// Simple: invert vy if hit top/bottom, vx if hit left/right
if (Math.abs(ball.vx) > Math.abs(ball.vy)) {
ball.vx *= -1;
} else {
ball.vy *= -1;
}
}
// Helper: reflect ball on paddle
function reflectBallPaddle(ball, paddle) {
// Calculate hit position relative to paddle center (-1 to 1)
var rel = (ball.x - paddle.x) / (paddle.width * (paddle.scaleX || 1) / 2);
rel = Math.max(-1, Math.min(1, rel));
var angle = rel * Math.PI / 3 + Math.PI / 2; // -60deg to +60deg from vertical
var speed = ball.speed;
ball.vx = speed * Math.sin(angle);
ball.vy = -Math.abs(speed * Math.cos(angle));
}
// Helper: spawn powerup
function spawnPowerup(x, y, type) {
var p = new Powerup();
p.x = x;
p.y = y;
if (type) p.type = type;
powerups.push(p);
game.addChild(p);
}
// Game move handler (drag paddle)
game.move = function (x, y, obj) {
if (gameOver) return;
// Clamp paddle to screen
var px = Math.max(paddle.width / 2, Math.min(GAME_W - paddle.width / 2, x));
paddle.x = px;
lastTouchX = px;
// If ball is stuck, move it too
for (var i = 0; i < balls.length; ++i) {
if (balls[i].stuck) balls[i].x = px;
}
};
// Game down handler (launch ball)
game.down = function (x, y, obj) {
if (gameOver) return;
if (ball && ball.stuck) {
launchBall();
}
};
// Main game update
game.update = function () {
if (gameOver) return;
// Update balls
for (var i = balls.length - 1; i >= 0; --i) {
var b = balls[i];
b.update();
// Wall collisions
if (b.x - b.radius < 0) {
b.x = b.radius;
b.vx = Math.abs(b.vx);
}
if (b.x + b.radius > GAME_W) {
b.x = GAME_W - b.radius;
b.vx = -Math.abs(b.vx);
}
if (b.y - b.radius < 0) {
b.y = b.radius;
b.vy = Math.abs(b.vy);
}
// Paddle collision
if (!b.stuck && rectsIntersect(b, paddle)) {
reflectBallPaddle(b, paddle);
// Nudge ball out of paddle
b.y = paddle.y - paddle.height / 2 - b.radius - 2;
}
// Block collisions
for (var j = blocks.length - 1; j >= 0; --j) {
var block = blocks[j];
if (block.destroyed) continue;
if (ballBlockCollision(b, block)) {
// Hit!
if (block.type !== 3) {
block.hits -= 1;
if (block.hits <= 0) {
block.destroyed = true;
tween(block, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
block.destroy();
}
});
blocks.splice(j, 1);
score += 100;
updateScore();
// Powerup drop
if (block.powerup) {
spawnPowerup(block.x, block.y, block.powerup);
}
// Win condition
var anyLeft = false;
for (var k = 0; k < blocks.length; ++k) {
if (blocks[k].type !== 3) {
anyLeft = true;
break;
}
}
if (!anyLeft) {
// Animate all remaining blocks out (milk animation)
for (var m = 0; m < blocks.length; ++m) {
var milkBlock = blocks[m];
tween(milkBlock, {
scaleY: 0.1,
alpha: 0
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function (blk) {
return function () {
blk.destroy();
};
}(milkBlock)
});
}
blocks = [];
// After animation, spawn a new wave of blocks for the next level
LK.setTimeout(function () {
// Level system: 30 levels, each with unique block patterns
if (typeof currentLevel === "undefined") {
currentLevel = 1;
} else {
currentLevel += 1;
}
if (currentLevel > 30) currentLevel = 30; // Cap at 30
// Helper: get block type for a given level, row, col
function getBlockTypeForLevel(level, row, col) {
// Level 1-5: simple, more normal, some strong, rare indestructible
if (level <= 5) {
if (row === 0 && col % 2 === 0) return 2;
if (row === 1 && col % 3 === 0) return 2;
if (row === 2 && col % 5 === 0) return 3;
return 1;
}
// Level 6-10: more strong, some indestructible
if (level <= 10) {
if ((row + col) % 4 === 0) return 3;
if ((row + col) % 2 === 0) return 2;
return 1;
}
// Level 11-15: checkerboard indestructible, strong
if (level <= 15) {
if ((row + col) % 2 === 0) return 3;
if (row * col % 3 === 0) return 2;
return 1;
}
// Level 16-20: border indestructible, center strong
if (level <= 20) {
if (row === 0 || row === BLOCK_ROWS - 1 || col === 0 || col === BLOCK_COLS - 1) return 3;
if ((row + col) % 2 === 1) return 2;
return 1;
}
// Level 21-25: stripes of indestructible, strong
if (level <= 25) {
if (col % 2 === 0) return 3;
if (row % 2 === 0) return 2;
return 1;
}
// Level 26-30: lots of indestructible, strong in center
if (level <= 30) {
if ((row === 1 || row === BLOCK_ROWS - 2) && (col === 2 || col === BLOCK_COLS - 3)) return 2;
if ((row + col) % 2 === 0) return 3;
return 1;
}
return 1;
}
// Helper: get powerup chance for a given level
function getPowerupChanceForLevel(level) {
if (level <= 5) return 0.20;
if (level <= 10) return 0.18;
if (level <= 15) return 0.16;
if (level <= 20) return 0.14;
if (level <= 25) return 0.12;
return 0.10;
}
// Show level number (optional: can be removed if not wanted)
if (!levelText) {
levelText = new Text2('Level ' + currentLevel, {
size: 120,
fill: "#fff"
});
levelText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(levelText);
} else {
levelText.setText('Level ' + currentLevel);
levelText.visible = true;
}
// Hide after 1s
LK.setTimeout(function () {
if (levelText) levelText.visible = false;
}, 1000);
// Block placement
var blockW = LK.getAsset('block1', {}).width;
var blockH = LK.getAsset('block1', {}).height;
var blockIndW = LK.getAsset('blockInd', {}).width;
var blockIndH = LK.getAsset('blockInd', {}).height;
var totalW = BLOCK_COLS * blockW + (BLOCK_COLS - 1) * BLOCK_MARGIN_X;
var startX = (GAME_W - totalW) / 2 + blockW / 2;
for (var row = 0; row < BLOCK_ROWS; ++row) {
for (var col = 0; col < BLOCK_COLS; ++col) {
var b = new Block();
// Use both block1 and block2 in every level, and make each level different
// Example: alternate block1/block2, and shift pattern by level for variety
if ((row + col + currentLevel) % 2 === 0) {
b.setType(1);
} else {
b.setType(2);
}
// Always use normal block size/position
b.x = startX + col * (blockW + BLOCK_MARGIN_X);
b.y = BLOCK_START_Y + row * (blockH + BLOCK_MARGIN_Y);
b.width = blockW;
b.height = blockH;
// Powerup chance by level
var powerupChance = getPowerupChanceForLevel(currentLevel);
if (Math.random() < powerupChance) b.powerup = true;
blocks.push(b);
game.addChild(b);
// Animate in (milk drop)
b.scaleY = 0.1;
b.alpha = 0;
tween(b, {
scaleY: 1,
alpha: 1
}, {
duration: 500,
easing: tween.easeOut
});
}
}
}, 550); // Wait for milk animation out
return;
}
}
}
// Flash block
LK.effects.flashObject(block, 0xffffff, 100);
// Reflect ball
reflectBall(b, block);
break;
}
}
// Ball falls below paddle
if (b.y - b.radius > GAME_H) {
b.destroy();
balls.splice(i, 1);
if (balls.length === 0) {
lives -= 1;
updateLives();
if (lives <= 0) {
gameOver = true;
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return;
} else {
spawnBall();
}
}
}
}
// Update powerups
for (var i = powerups.length - 1; i >= 0; --i) {
var p = powerups[i];
p.update();
// Paddle catch
if (rectsIntersect(p, paddle)) {
// Powerup effect logic
if (p.type === 'extend') {
paddle.extend();
} else if (p.type === 'shrink') {
// Shrink paddle for 7s
if (!paddle.shrunk) {
paddle.shrunk = true;
tween(paddle, {
scaleX: 0.6
}, {
duration: 300,
easing: tween.easeOut
});
if (paddle.shrinkTimeout) LK.clearTimeout(paddle.shrinkTimeout);
paddle.shrinkTimeout = LK.setTimeout(function () {
tween(paddle, {
scaleX: 1
}, {
duration: 300,
easing: tween.easeIn
});
paddle.shrunk = false;
}, 7000);
}
} else if (p.type === 'multi') {
// Multi-ball: spawn 2 extra balls
for (var mb = 0; mb < 2; ++mb) {
var newBall = new Ball();
newBall.x = ball.x;
newBall.y = ball.y;
newBall.stuck = false;
var angle = (Math.random() * 0.5 + 0.25) * Math.PI;
newBall.vx = ball.speed * Math.cos(angle) * (Math.random() < 0.5 ? 1 : -1);
newBall.vy = -ball.speed * Math.abs(Math.sin(angle));
balls.push(newBall);
game.addChild(newBall);
}
} else if (p.type === 'slow') {
// Slow all balls for 7s
for (var sb = 0; sb < balls.length; ++sb) {
var b = balls[sb];
if (!b.slowed) {
b.slowed = true;
b.speed = 12;
var mag = Math.sqrt(b.vx * b.vx + b.vy * b.vy);
if (mag > 0) {
b.vx *= 12 / mag;
b.vy *= 12 / mag;
}
}
}
if (!game.slowTimeout) {
game.slowTimeout = LK.setTimeout(function () {
for (var sb = 0; sb < balls.length; ++sb) {
var b = balls[sb];
if (b.slowed) {
b.speed = 22;
b.slowed = false;
var mag = Math.sqrt(b.vx * b.vx + b.vy * b.vy);
if (mag > 0) {
b.vx *= 22 / mag;
b.vy *= 22 / mag;
}
}
}
game.slowTimeout = null;
}, 7000);
}
} else if (p.type === 'fast') {
// Speed up all balls for 7s
for (var fb = 0; fb < balls.length; ++fb) {
var b = balls[fb];
if (!b.fasted) {
b.fasted = true;
b.speed = 36;
var mag = Math.sqrt(b.vx * b.vx + b.vy * b.vy);
if (mag > 0) {
b.vx *= 36 / mag;
b.vy *= 36 / mag;
}
}
}
if (!game.fastTimeout) {
game.fastTimeout = LK.setTimeout(function () {
for (var fb = 0; fb < balls.length; ++fb) {
var b = balls[fb];
if (b.fasted) {
b.speed = 22;
b.fasted = false;
var mag = Math.sqrt(b.vx * b.vx + b.vy * b.vy);
if (mag > 0) {
b.vx *= 22 / mag;
b.vy *= 22 / mag;
}
}
}
game.fastTimeout = null;
}, 7000);
}
} else if (p.type === 'catch') {
// Sticky paddle for 7s
paddle.sticky = true;
if (paddle.stickyTimeout) LK.clearTimeout(paddle.stickyTimeout);
paddle.stickyTimeout = LK.setTimeout(function () {
paddle.sticky = false;
}, 7000);
} else if (p.type === 'life') {
// Extra life
lives += 1;
updateLives();
} else if (p.type === 'bigball') {
// Make all balls big for 7s
for (var bb = 0; bb < balls.length; ++bb) {
var b = balls[bb];
if (!b.bigged) {
b.bigged = true;
b.scaleX = b.scaleY = 1.5;
b.radius = LK.getAsset('ball', {}).width * 0.75;
}
}
if (!game.bigTimeout) {
game.bigTimeout = LK.setTimeout(function () {
for (var bb = 0; bb < balls.length; ++bb) {
var b = balls[bb];
if (b.bigged) {
b.scaleX = b.scaleY = 1;
b.radius = LK.getAsset('ball', {}).width / 2;
b.bigged = false;
}
}
game.bigTimeout = null;
}, 7000);
}
} else if (p.type === 'smallball') {
// Make all balls small for 7s
for (var sb = 0; sb < balls.length; ++sb) {
var b = balls[sb];
if (!b.smalled) {
b.smalled = true;
b.scaleX = b.scaleY = 0.6;
b.radius = LK.getAsset('ball', {}).width * 0.3;
}
}
if (!game.smallTimeout) {
game.smallTimeout = LK.setTimeout(function () {
for (var sb = 0; sb < balls.length; ++sb) {
var b = balls[sb];
if (b.smalled) {
b.scaleX = b.scaleY = 1;
b.radius = LK.getAsset('ball', {}).width / 2;
b.smalled = false;
}
}
game.smallTimeout = null;
}, 7000);
}
} else if (p.type === 'score') {
// Bonus score
score += 500;
updateScore();
}
p.destroy();
powerups.splice(i, 1);
} else if (p.y - p.height / 2 > GAME_H) {
p.destroy();
powerups.splice(i, 1);
}
}
};
// Start game
resetGame();