/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball class
var Ball = Container.expand(function () {
var self = Container.call(this);
// Ball properties
self.size = 1; // 1-9
self.radius = 60; // default, will be set in init
self.vx = 0;
self.vy = 0;
self.isMerging = false; // Prevent double merge
// Attach asset based on size
self.setSize = function (size) {
self.size = size;
// Remove previous asset if any
if (self.ballAsset) {
self.removeChild(self.ballAsset);
}
var assetId = 'ball' + size;
self.ballAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.ballAsset.width / 2;
};
// Set initial size
self.setSize(1);
// For merge animation
self.flash = function () {
tween(self.ballAsset, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.ballAsset, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
};
// For showing value
self.showValue = function () {
if (self.valueText) {
self.removeChild(self.valueText);
}
self.valueText = new Text2('' + self.size, {
size: Math.max(60, self.radius),
fill: 0xFFFFFF
});
self.valueText.anchor.set(0.5, 0.5);
self.addChild(self.valueText);
};
self.showValue();
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222831
});
/****
* Game Code
****/
// Game constants
// Ball shapes for different sizes (size1, size2, size3, size4, size5, size6, size7, size8, size9)
// Sound for merging
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var BALL_MIN_SIZE = 1;
var BALL_MAX_SIZE = 12; // Now only up to 3 new big balls (ball10, ball11, ball12)
var BALL_LIMIT = 16; // Max balls allowed on screen before game over
// var INIT_BALLS = 9; // No longer needed, no initial balls
var GRAVITY = 1.2;
var BOUNCE = 0.7;
var FRICTION = 0.995;
var WALL_BOUNCE = 0.8;
var SPAWN_Y = 180; // y position for new balls
// Ball array
var balls = [];
// Score
var score = 0;
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Next ball preview
var nextBallSize = BALL_MIN_SIZE + Math.floor(Math.random() * 4); // Only 1-4
var nextBallPreview = LK.getAsset('ball' + nextBallSize, {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_WIDTH / 2,
y: SPAWN_Y - 100
});
game.addChild(nextBallPreview);
// Helper: get random ball size (only 1-4 for spawn)
function getRandomBallSize() {
// Weighted random: 1: 45%, 2: 30%, 3: 15%, 4: 10%
var r = Math.random();
if (r < 0.45) return 1;
if (r < 0.75) return 2;
if (r < 0.90) return 3;
return 4;
}
// Helper: clamp
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// Helper: distance squared
function dist2(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return dx * dx + dy * dy;
}
// Helper: check overlap
function isOverlapping(ballA, ballB) {
var rSum = ballA.radius + ballB.radius;
return dist2(ballA, ballB) < rSum * rSum - 1;
}
// Helper: resolve overlap
function resolveOverlap(ballA, ballB) {
var dx = ballB.x - ballA.x;
var dy = ballB.y - ballA.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) {
// Random nudge
dx = 1;
dy = 0;
dist = 1;
}
var overlap = ballA.radius + ballB.radius - dist;
if (overlap > 0) {
var pushX = dx / dist * (overlap / 2);
var pushY = dy / dist * (overlap / 2);
ballA.x -= pushX;
ballA.y -= pushY;
ballB.x += pushX;
ballB.y += pushY;
}
}
// Helper: merge balls
function mergeBalls(ballA, ballB) {
// Only merge if both are not merging and same size
if (ballA.isMerging || ballB.isMerging) return false;
if (ballA.size !== ballB.size) return false;
ballA.isMerging = true;
ballB.isMerging = true;
// New ball at average position
var newBall = new Ball();
var newSize = clamp(ballA.size + 1, BALL_MIN_SIZE, BALL_MAX_SIZE);
newBall.setSize(newSize);
newBall.x = (ballA.x + ballB.x) / 2;
newBall.y = (ballA.y + ballB.y) / 2;
newBall.vx = (ballA.vx + ballB.vx) / 2;
newBall.vy = (ballA.vy + ballB.vy) / 2;
newBall.showValue();
newBall.flash();
// Remove old balls
ballA.destroy();
ballB.destroy();
var idxA = balls.indexOf(ballA);
var idxB = balls.indexOf(ballB);
if (idxA !== -1) balls.splice(idxA, 1);
if (idxB !== -1) balls.splice(balls.indexOf(ballB), 1); // index may have shifted
// Add new ball
balls.push(newBall);
game.addChild(newBall);
// Play merge sound
LK.getSound('merge').play();
// Add score
score += Math.pow(2, newSize);
scoreTxt.setText(score);
// Animate
newBall.flash();
// Feature 9: Shake nearby balls after merge
var SHAKE_RADIUS = newBall.radius * 3;
for (var i = 0; i < balls.length; i++) {
var b = balls[i];
if (b !== newBall && !b.isMerging) {
var dx = b.x - newBall.x;
var dy = b.y - newBall.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < SHAKE_RADIUS) {
// Give a small random velocity burst
var angle = Math.atan2(dy, dx);
var force = 10 + Math.random() * 10;
b.vx += Math.cos(angle) * force * (0.7 + 0.6 * Math.random());
b.vy += Math.sin(angle) * force * (0.7 + 0.6 * Math.random());
}
}
}
return newBall;
}
// Spawn a ball at (x, y) with given size
function spawnBall(x, y, size) {
var ball = new Ball();
ball.setSize(size);
ball.x = clamp(x, ball.radius, GAME_WIDTH - ball.radius);
ball.y = y;
ball.vx = (Math.random() - 0.5) * 16;
ball.vy = 0;
ball.showValue();
balls.push(ball);
game.addChild(ball);
return ball;
}
// No initial balls function needed
// Drag and drop for new balls
var isDragging = false;
var dragX = GAME_WIDTH / 2;
var dragBall = null;
// Show drag preview
var dragPreview = null;
function showDragPreview(x) {
if (!dragPreview) {
dragPreview = LK.getAsset('ball' + nextBallSize, {
anchorX: 0.5,
anchorY: 0.5,
x: x,
y: SPAWN_Y
});
game.addChild(dragPreview);
}
dragPreview.x = clamp(x, dragPreview.width / 2, GAME_WIDTH - dragPreview.width / 2);
dragPreview.y = SPAWN_Y;
}
function hideDragPreview() {
if (dragPreview) {
dragPreview.destroy();
dragPreview = null;
}
}
// Handle touch/mouse down
game.down = function (x, y, obj) {
// Only allow drop in top 1/3 of screen
if (y > GAME_HEIGHT / 2) return;
isDragging = true;
dragX = clamp(x, 100, GAME_WIDTH - 100);
showDragPreview(dragX);
};
// Handle move
game.move = function (x, y, obj) {
if (isDragging) {
dragX = clamp(x, 100, GAME_WIDTH - 100);
showDragPreview(dragX);
}
};
// Handle up (drop ball)
game.up = function (x, y, obj) {
if (isDragging) {
isDragging = false;
hideDragPreview();
// Drop ball at dragX
spawnBall(dragX, SPAWN_Y, nextBallSize);
// Next ball
nextBallSize = getRandomBallSize();
// Update preview
if (nextBallPreview) {
nextBallPreview.destroy();
}
nextBallPreview = LK.getAsset('ball' + nextBallSize, {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_WIDTH / 2,
y: SPAWN_Y - 100
});
game.addChild(nextBallPreview);
}
};
// Main update loop
game.update = function () {
// Physics for each ball
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
// Gravity
ball.vy += GRAVITY;
// Move
ball.x += ball.vx;
ball.y += ball.vy;
// Wall collision
if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
ball.vx = -ball.vx * WALL_BOUNCE;
}
if (ball.x + ball.radius > GAME_WIDTH) {
ball.x = GAME_WIDTH - ball.radius;
ball.vx = -ball.vx * WALL_BOUNCE;
}
// Floor
if (ball.y + ball.radius > GAME_HEIGHT) {
ball.y = GAME_HEIGHT - ball.radius;
ball.vy = -ball.vy * BOUNCE;
ball.vx *= FRICTION;
// Stop tiny bounces
if (Math.abs(ball.vy) < 2) ball.vy = 0;
}
// Ceiling
if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
ball.vy = -ball.vy * BOUNCE;
}
// Friction
ball.vx *= FRICTION;
ball.vy *= FRICTION;
}
// Ball-ball collision and merging
for (var i = 0; i < balls.length; i++) {
for (var j = i + 1; j < balls.length; j++) {
var a = balls[i],
b = balls[j];
if (isOverlapping(a, b)) {
// If same size, merge
if (a.size === b.size && !a.isMerging && !b.isMerging) {
mergeBalls(a, b);
break; // balls array changed, restart
} else {
// Elastic collision
resolveOverlap(a, b);
// Simple velocity swap
var dx = b.x - a.x;
var dy = b.y - a.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
var nx = dx / dist,
ny = dy / dist;
var p = 2 * (a.vx * nx + a.vy * ny - b.vx * nx - b.vy * ny) / 2;
a.vx = a.vx - p * nx;
a.vy = a.vy - p * ny;
b.vx = b.vx + p * nx;
b.vy = b.vy + p * ny;
}
}
}
}
}
// Remove balls marked for destroy (already handled in mergeBalls)
// Check for game over
if (balls.length > BALL_LIMIT) {
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
}
// Optionally: win condition (e.g. create a ball of size 12)
for (var i = 0; i < balls.length; i++) {
if (balls[i].size === 12) {
LK.effects.flashScreen(0xffff00, 1000);
LK.showYouWin();
break;
}
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball class
var Ball = Container.expand(function () {
var self = Container.call(this);
// Ball properties
self.size = 1; // 1-9
self.radius = 60; // default, will be set in init
self.vx = 0;
self.vy = 0;
self.isMerging = false; // Prevent double merge
// Attach asset based on size
self.setSize = function (size) {
self.size = size;
// Remove previous asset if any
if (self.ballAsset) {
self.removeChild(self.ballAsset);
}
var assetId = 'ball' + size;
self.ballAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.ballAsset.width / 2;
};
// Set initial size
self.setSize(1);
// For merge animation
self.flash = function () {
tween(self.ballAsset, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.ballAsset, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
};
// For showing value
self.showValue = function () {
if (self.valueText) {
self.removeChild(self.valueText);
}
self.valueText = new Text2('' + self.size, {
size: Math.max(60, self.radius),
fill: 0xFFFFFF
});
self.valueText.anchor.set(0.5, 0.5);
self.addChild(self.valueText);
};
self.showValue();
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222831
});
/****
* Game Code
****/
// Game constants
// Ball shapes for different sizes (size1, size2, size3, size4, size5, size6, size7, size8, size9)
// Sound for merging
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var BALL_MIN_SIZE = 1;
var BALL_MAX_SIZE = 12; // Now only up to 3 new big balls (ball10, ball11, ball12)
var BALL_LIMIT = 16; // Max balls allowed on screen before game over
// var INIT_BALLS = 9; // No longer needed, no initial balls
var GRAVITY = 1.2;
var BOUNCE = 0.7;
var FRICTION = 0.995;
var WALL_BOUNCE = 0.8;
var SPAWN_Y = 180; // y position for new balls
// Ball array
var balls = [];
// Score
var score = 0;
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Next ball preview
var nextBallSize = BALL_MIN_SIZE + Math.floor(Math.random() * 4); // Only 1-4
var nextBallPreview = LK.getAsset('ball' + nextBallSize, {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_WIDTH / 2,
y: SPAWN_Y - 100
});
game.addChild(nextBallPreview);
// Helper: get random ball size (only 1-4 for spawn)
function getRandomBallSize() {
// Weighted random: 1: 45%, 2: 30%, 3: 15%, 4: 10%
var r = Math.random();
if (r < 0.45) return 1;
if (r < 0.75) return 2;
if (r < 0.90) return 3;
return 4;
}
// Helper: clamp
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
// Helper: distance squared
function dist2(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return dx * dx + dy * dy;
}
// Helper: check overlap
function isOverlapping(ballA, ballB) {
var rSum = ballA.radius + ballB.radius;
return dist2(ballA, ballB) < rSum * rSum - 1;
}
// Helper: resolve overlap
function resolveOverlap(ballA, ballB) {
var dx = ballB.x - ballA.x;
var dy = ballB.y - ballA.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) {
// Random nudge
dx = 1;
dy = 0;
dist = 1;
}
var overlap = ballA.radius + ballB.radius - dist;
if (overlap > 0) {
var pushX = dx / dist * (overlap / 2);
var pushY = dy / dist * (overlap / 2);
ballA.x -= pushX;
ballA.y -= pushY;
ballB.x += pushX;
ballB.y += pushY;
}
}
// Helper: merge balls
function mergeBalls(ballA, ballB) {
// Only merge if both are not merging and same size
if (ballA.isMerging || ballB.isMerging) return false;
if (ballA.size !== ballB.size) return false;
ballA.isMerging = true;
ballB.isMerging = true;
// New ball at average position
var newBall = new Ball();
var newSize = clamp(ballA.size + 1, BALL_MIN_SIZE, BALL_MAX_SIZE);
newBall.setSize(newSize);
newBall.x = (ballA.x + ballB.x) / 2;
newBall.y = (ballA.y + ballB.y) / 2;
newBall.vx = (ballA.vx + ballB.vx) / 2;
newBall.vy = (ballA.vy + ballB.vy) / 2;
newBall.showValue();
newBall.flash();
// Remove old balls
ballA.destroy();
ballB.destroy();
var idxA = balls.indexOf(ballA);
var idxB = balls.indexOf(ballB);
if (idxA !== -1) balls.splice(idxA, 1);
if (idxB !== -1) balls.splice(balls.indexOf(ballB), 1); // index may have shifted
// Add new ball
balls.push(newBall);
game.addChild(newBall);
// Play merge sound
LK.getSound('merge').play();
// Add score
score += Math.pow(2, newSize);
scoreTxt.setText(score);
// Animate
newBall.flash();
// Feature 9: Shake nearby balls after merge
var SHAKE_RADIUS = newBall.radius * 3;
for (var i = 0; i < balls.length; i++) {
var b = balls[i];
if (b !== newBall && !b.isMerging) {
var dx = b.x - newBall.x;
var dy = b.y - newBall.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < SHAKE_RADIUS) {
// Give a small random velocity burst
var angle = Math.atan2(dy, dx);
var force = 10 + Math.random() * 10;
b.vx += Math.cos(angle) * force * (0.7 + 0.6 * Math.random());
b.vy += Math.sin(angle) * force * (0.7 + 0.6 * Math.random());
}
}
}
return newBall;
}
// Spawn a ball at (x, y) with given size
function spawnBall(x, y, size) {
var ball = new Ball();
ball.setSize(size);
ball.x = clamp(x, ball.radius, GAME_WIDTH - ball.radius);
ball.y = y;
ball.vx = (Math.random() - 0.5) * 16;
ball.vy = 0;
ball.showValue();
balls.push(ball);
game.addChild(ball);
return ball;
}
// No initial balls function needed
// Drag and drop for new balls
var isDragging = false;
var dragX = GAME_WIDTH / 2;
var dragBall = null;
// Show drag preview
var dragPreview = null;
function showDragPreview(x) {
if (!dragPreview) {
dragPreview = LK.getAsset('ball' + nextBallSize, {
anchorX: 0.5,
anchorY: 0.5,
x: x,
y: SPAWN_Y
});
game.addChild(dragPreview);
}
dragPreview.x = clamp(x, dragPreview.width / 2, GAME_WIDTH - dragPreview.width / 2);
dragPreview.y = SPAWN_Y;
}
function hideDragPreview() {
if (dragPreview) {
dragPreview.destroy();
dragPreview = null;
}
}
// Handle touch/mouse down
game.down = function (x, y, obj) {
// Only allow drop in top 1/3 of screen
if (y > GAME_HEIGHT / 2) return;
isDragging = true;
dragX = clamp(x, 100, GAME_WIDTH - 100);
showDragPreview(dragX);
};
// Handle move
game.move = function (x, y, obj) {
if (isDragging) {
dragX = clamp(x, 100, GAME_WIDTH - 100);
showDragPreview(dragX);
}
};
// Handle up (drop ball)
game.up = function (x, y, obj) {
if (isDragging) {
isDragging = false;
hideDragPreview();
// Drop ball at dragX
spawnBall(dragX, SPAWN_Y, nextBallSize);
// Next ball
nextBallSize = getRandomBallSize();
// Update preview
if (nextBallPreview) {
nextBallPreview.destroy();
}
nextBallPreview = LK.getAsset('ball' + nextBallSize, {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_WIDTH / 2,
y: SPAWN_Y - 100
});
game.addChild(nextBallPreview);
}
};
// Main update loop
game.update = function () {
// Physics for each ball
for (var i = 0; i < balls.length; i++) {
var ball = balls[i];
// Gravity
ball.vy += GRAVITY;
// Move
ball.x += ball.vx;
ball.y += ball.vy;
// Wall collision
if (ball.x - ball.radius < 0) {
ball.x = ball.radius;
ball.vx = -ball.vx * WALL_BOUNCE;
}
if (ball.x + ball.radius > GAME_WIDTH) {
ball.x = GAME_WIDTH - ball.radius;
ball.vx = -ball.vx * WALL_BOUNCE;
}
// Floor
if (ball.y + ball.radius > GAME_HEIGHT) {
ball.y = GAME_HEIGHT - ball.radius;
ball.vy = -ball.vy * BOUNCE;
ball.vx *= FRICTION;
// Stop tiny bounces
if (Math.abs(ball.vy) < 2) ball.vy = 0;
}
// Ceiling
if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
ball.vy = -ball.vy * BOUNCE;
}
// Friction
ball.vx *= FRICTION;
ball.vy *= FRICTION;
}
// Ball-ball collision and merging
for (var i = 0; i < balls.length; i++) {
for (var j = i + 1; j < balls.length; j++) {
var a = balls[i],
b = balls[j];
if (isOverlapping(a, b)) {
// If same size, merge
if (a.size === b.size && !a.isMerging && !b.isMerging) {
mergeBalls(a, b);
break; // balls array changed, restart
} else {
// Elastic collision
resolveOverlap(a, b);
// Simple velocity swap
var dx = b.x - a.x;
var dy = b.y - a.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
var nx = dx / dist,
ny = dy / dist;
var p = 2 * (a.vx * nx + a.vy * ny - b.vx * nx - b.vy * ny) / 2;
a.vx = a.vx - p * nx;
a.vy = a.vy - p * ny;
b.vx = b.vx + p * nx;
b.vy = b.vy + p * ny;
}
}
}
}
}
// Remove balls marked for destroy (already handled in mergeBalls)
// Check for game over
if (balls.length > BALL_LIMIT) {
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
}
// Optionally: win condition (e.g. create a ball of size 12)
for (var i = 0; i < balls.length; i++) {
if (balls[i].size === 12) {
LK.effects.flashScreen(0xffff00, 1000);
LK.showYouWin();
break;
}
}
};
Pinpon ball. In-Game asset. 2d. High contrast. No shadows
tennis ball. In-Game asset. 2d. High contrast. No shadows
american football ball. In-Game asset. 2d. High contrast. No shadows
volleyball ball. In-Game asset. 2d. High contrast. No shadows
football ball. In-Game asset. 2d. High contrast. No shadows
basketball ball. In-Game asset. 2d. High contrast. No shadows
dodge ball ball. In-Game asset. 2d. High contrast. No shadows
medicine ball. In-Game asset. 2d. High contrast. No shadows
yoga ball. In-Game asset. 2d. High contrast. No shadows
beach ball. In-Game asset. 2d. High contrast. No shadows
zorbing ball. In-Game asset. 2d. High contrast. No shadows
flying air balloon. In-Game asset. 2d. High contrast. No shadows