/****
* 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();