/**** * 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.active = true; self.update = function () { if (!self.active) return; // Simple gravity self.vy += 0.5; // slower gravity self.x += self.vx; self.y += self.vy; }; // Used to mark as collected or destroyed self.deactivate = function () { self.active = false; }; return self; }); // Bottom cup class (collector) var CupBottom = Container.expand(function () { var self = Container.call(this); // Add border using a slightly larger box behind var border = self.attachAsset('cupBottom', { anchorX: 0.5, anchorY: 0.5, width: 2048 + 16, height: 100 + 16, color: 0x222222 }); var cupGfx = self.attachAsset('cupBottom', { anchorX: 0.5, anchorY: 0.5 }); self.setChildIndex(border, 0); self.width = cupGfx.width; self.height = cupGfx.height; return self; }); // Top cup class (movable) var CupTop = Container.expand(function () { var self = Container.call(this); var cupGfx = self.attachAsset('cupTop', { anchorX: 0.5, anchorY: 0.5 }); self.width = cupGfx.width; self.height = cupGfx.height; // Add border using a slightly larger box behind var border = self.attachAsset('cupTop', { anchorX: 0.5, anchorY: 0.5, width: cupGfx.width + 12, height: cupGfx.height + 12, color: 0x222222 }); self.setChildIndex(border, 0); return self; }); // Gate class (multiplier or divider) var Gate = Container.expand(function () { var self = Container.call(this); self.type = 'mult'; // 'mult' or 'div' self.value = 2; // e.g. 2x or /2 self.width = 180; self.height = 40; self.gfx = null; self.label = null; self.leftBorder = null; self.rightBorder = null; self.init = function (type, value) { self.type = type; self.value = value; if (type === 'mult') { self.gfx = self.attachAsset('gateMult', { anchorX: 0.5, anchorY: 0.5 }); } else if (type === 'div') { self.gfx = self.attachAsset('gateDiv', { anchorX: 0.5, anchorY: 0.5 }); } else if (type === 'reverse') { self.gfx = self.attachAsset('gateDiv', { anchorX: 0.5, anchorY: 0.5, color: 0x3498db // blue for reverse }); } else if (type === 'speed') { self.gfx = self.attachAsset('gateMult', { anchorX: 0.5, anchorY: 0.5, color: 0x27ae60 // green for speed }); } else if (type === 'bounce') { self.gfx = self.attachAsset('gateMult', { anchorX: 0.5, anchorY: 0.5, color: 0xe74c3c // red for bounce }); } var txt = ''; if (type === 'mult') { txt = 'x' + value; } else if (type === 'div') { txt = '÷' + value; } else if (type === 'reverse') { txt = 'REV'; } else if (type === 'speed') { txt = value < 1 ? value + 'x SLOW' : value + 'x FAST'; } else if (type === 'bounce') { txt = 'BOUNCE'; } self.label = new Text2(txt, { size: 60, fill: 0xFFFFFF }); self.label.anchor.set(0.5, 0.5); self.addChild(self.label); // Add physical vertical borders to the sides of the gate // Use a tall, thin box for each border var borderWidth = 32; var borderHeight = self.gfx.height + 60; self.leftBorder = self.attachAsset('cupTop', { anchorX: 0.5, anchorY: 0.5, width: borderWidth, height: borderHeight, color: 0x222222 }); self.rightBorder = self.attachAsset('cupTop', { anchorX: 0.5, anchorY: 0.5, width: borderWidth, height: borderHeight, color: 0x222222 }); // Position borders at left/right edge of gate self.leftBorder.x = -self.gfx.width / 2 - borderWidth / 2 + 2; self.leftBorder.y = 0; self.rightBorder.x = self.gfx.width / 2 + borderWidth / 2 - 2; self.rightBorder.y = 0; // Ensure borders are behind the gate graphic self.setChildIndex(self.leftBorder, 0); self.setChildIndex(self.rightBorder, 0); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222b3a }); /**** * Game Code ****/ // Game constants // Top cup (where balls are dropped from) // Bottom cup (where balls are collected) // Ball // Multiplier gate // Divider gate var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; var BALL_DROP_COUNT = 10; var BALL_DROP_INTERVAL = 8; // frames between balls // Randomize gate positions and types var GATE_COUNT = 6; var GATE_MIN_Y = 500; var GATE_MAX_Y = GAME_HEIGHT - 600; var GATE_SPACING_MIN = 300; var GATE_SPACING_MAX = 500; var GATE_TYPES = [{ type: 'mult', values: [2, 3, 4, 5] }, { type: 'div', values: [2] }, { type: 'reverse', values: [1] // no value needed, but keep for consistency }, { type: 'speed', values: [0.5, 1.5, 2] // 0.5x (slower), 1.5x, 2x (faster) }, { type: 'bounce', values: [1] // no value needed, but keep for consistency }]; function randomizeGates() { var configs = []; var lastY = GATE_MIN_Y - 200; for (var i = 0; i < GATE_COUNT; i++) { // Randomly pick type var typeIdx = Math.floor(Math.random() * GATE_TYPES.length); var typeObj = GATE_TYPES[typeIdx]; var value = typeObj.values[Math.floor(Math.random() * typeObj.values.length)]; // Randomly space Y var spacing = GATE_SPACING_MIN + Math.floor(Math.random() * (GATE_SPACING_MAX - GATE_SPACING_MIN)); var y = lastY + spacing; if (y > GATE_MAX_Y) y = GATE_MAX_Y; lastY = y; configs.push({ type: typeObj.type, value: value, y: y }); } return configs; } var GATE_CONFIGS = randomizeGates(); // Game state var cupTop, cupBottom; var balls = []; var gates = []; var dropping = false; var dropTimer = 0; var ballsToDrop = 0; var scoreTxt, infoTxt; var collectedCount = 0; var gatePassed = {}; // ball.id -> {gateIndex: true} var ballIdCounter = 0; var dragNode = null; // Add border walls to constrain balls var leftBorder = game.attachAsset('cupTop', { anchorX: 0.5, anchorY: 0.5, width: 60, height: GAME_HEIGHT - 200, color: 0x222222 }); leftBorder.x = 30; leftBorder.y = GAME_HEIGHT / 2; game.addChild(leftBorder); var rightBorder = game.attachAsset('cupTop', { anchorX: 0.5, anchorY: 0.5, width: 60, height: GAME_HEIGHT - 200, color: 0x222222 }); rightBorder.x = GAME_WIDTH - 30; rightBorder.y = GAME_HEIGHT / 2; game.addChild(rightBorder); // Create top cup cupTop = new CupTop(); cupTop.x = GAME_WIDTH / 2; cupTop.y = 250; game.addChild(cupTop); // Create bottom cup cupBottom = new CupBottom(); cupBottom.x = GAME_WIDTH / 2; cupBottom.y = GAME_HEIGHT - 180; game.addChild(cupBottom); // Create gates for (var i = 0; i < GATE_CONFIGS.length; i++) { var g = new Gate(); g.init(GATE_CONFIGS[i].type, GATE_CONFIGS[i].value); g.x = GAME_WIDTH / 2; g.y = GATE_CONFIGS[i].y; // Add movement state g.moveDir = Math.random() < 0.5 ? 1 : -1; // 1:right, -1:left g.moveSpeed = 4 + Math.random() * 2; // px per frame g.lastX = g.x; game.addChild(g); gates.push(g); } // Score text (balls collected) scoreTxt = new Text2('Balls: 0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Info text (instructions) infoTxt = new Text2('Drag cup & tap to drop balls!', { size: 60, fill: 0xFFFFFF }); infoTxt.anchor.set(0.5, 0); LK.gui.top.addChild(infoTxt); // Position GUI elements scoreTxt.y = 120; infoTxt.y = 260; // Helper: spawn a single ball at cupTop function spawnBall(x, y, vx, vy) { var b = new Ball(); b.x = x; b.y = y; b.vx = vx || 0; b.vy = vy || 0; b.id = ++ballIdCounter; balls.push(b); game.addChild(b); gatePassed[b.id] = {}; return b; } // Start dropping balls function startDrop() { if (dropping) return; dropping = true; ballsToDrop = BALL_DROP_COUNT; dropTimer = 0; infoTxt.setText('Balls dropping...'); } // Handle drag/move of cup function handleMove(x, y, obj) { if (dragNode) { // Clamp cup within screen, avoid top left 100x100 var minX = 150 + dragNode.width / 2; var maxX = GAME_WIDTH - dragNode.width / 2 - 50; dragNode.x = Math.max(minX, Math.min(maxX, x)); } } // Touch/drag events game.down = function (x, y, obj) { // If touch is on cupTop, start drag var local = cupTop.toLocal(game.toGlobal({ x: x, y: y })); if (local.x > -cupTop.width / 2 && local.x < cupTop.width / 2 && local.y > -cupTop.height / 2 && local.y < cupTop.height / 2) { dragNode = cupTop; } }; game.move = function (x, y, obj) { handleMove(x, y, obj); }; game.up = function (x, y, obj) { dragNode = null; // If not dragging, and not dropping, start drop on tap if (!dropping) { startDrop(); } }; // Main update loop game.update = function () { // Handle dropping balls if (dropping && ballsToDrop > 0) { dropTimer--; if (dropTimer <= 0) { // Drop a ball from cupTop center, with smaller random vx and lower initial vy var vx = (Math.random() - 0.5) * 3; var b = spawnBall(cupTop.x, cupTop.y + cupTop.height / 2 + 10, vx, 1); ballsToDrop--; dropTimer = BALL_DROP_INTERVAL; } } // Move gates side to side and prevent overlap for (var i = 0; i < gates.length; i++) { var g = gates[i]; g.lastX = g.x; // Move g.x += g.moveDir * g.moveSpeed; // Clamp to screen var minX = g.gfx.width / 2 + 60; var maxX = GAME_WIDTH - g.gfx.width / 2 - 60; if (g.x < minX) { g.x = minX; g.moveDir = 1; } if (g.x > maxX) { g.x = maxX; g.moveDir = -1; } } // Prevent overlap between gates (simple separation, left-to-right) for (var i = 0; i < gates.length; i++) { for (var j = i + 1; j < gates.length; j++) { var g1 = gates[i]; var g2 = gates[j]; // Only check if y overlap (vertical proximity) if (Math.abs(g1.y - g2.y) < 120) { var minDist = (g1.gfx.width + g2.gfx.width) / 2 + 40; var dx = g2.x - g1.x; if (Math.abs(dx) < minDist) { // Push apart var overlap = minDist - Math.abs(dx); var push = overlap / 2; if (dx === 0) dx = (Math.random() < 0.5 ? -1 : 1) * 0.1; // avoid zero var dir = dx > 0 ? 1 : -1; g1.x -= push * dir; g2.x += push * dir; // Clamp after push var minX1 = g1.gfx.width / 2 + 60; var maxX1 = GAME_WIDTH - g1.gfx.width / 2 - 60; var minX2 = g2.gfx.width / 2 + 60; var maxX2 = GAME_WIDTH - g2.gfx.width / 2 - 60; g1.x = Math.max(minX1, Math.min(maxX1, g1.x)); g2.x = Math.max(minX2, Math.min(maxX2, g2.x)); // Reverse directions if they crash g1.moveDir *= -1; g2.moveDir *= -1; } } } } // Show current number of balls in play (real time, not just collected) scoreTxt.setText('Balls: ' + (collectedCount + balls.length)); for (var i = balls.length - 1; i >= 0; i--) { var b = balls[i]; b.update(); // Check for gate collisions for (var gidx = 0; gidx < gates.length; gidx++) { var g = gates[gidx]; // Only process if not already passed if (!gatePassed[b.id][gidx]) { // Simple AABB collision if (b.x + b.radius > g.x - g.width / 2 && b.x - b.radius < g.x + g.width / 2 && b.y + b.radius > g.y - g.height / 2 && b.y - b.radius < g.y + g.height / 2) { // Mark as passed gatePassed[b.id][gidx] = true; // Apply gate effect if (g.type === 'mult') { // Multiply: spawn (value-1) new balls at same position for (var m = 1; m < g.value; m++) { // Spread balls more: wider vx, add a little vy randomness var spreadVx = (Math.random() - 0.5) * 14; var spreadVy = b.vy * 0.7 + (Math.random() - 0.5) * 2; var nb = spawnBall(b.x, b.y, spreadVx, spreadVy); // Copy gatePassed so new balls don't re-trigger previous gates for (var k in gatePassed[b.id]) { gatePassed[nb.id][k] = true; } } } else if (g.type === 'div') { // Divide: only keep 1/value balls, remove others // Remove this ball if Math.random() > 1/value if (Math.random() > 1 / g.value) { b.deactivate(); } } else if (g.type === 'reverse') { // Reverse: flip vx and vy b.vx = -b.vx; b.vy = -Math.abs(b.vy) * 0.7; // send upward, reduce speed } else if (g.type === 'speed') { // Speed: multiply vy and vx by value b.vx *= g.value; b.vy *= g.value; } else if (g.type === 'bounce') { // Bounce: invert vy, reduce speed, add a little random b.vy = -Math.abs(b.vy) * (0.7 + Math.random() * 0.2); b.vx += (Math.random() - 0.5) * 6; } } } } // Remove deactivated balls if (!b.active) { b.destroy(); balls.splice(i, 1); continue; } // Check for collection in bottom cup if (b.y + b.radius > cupBottom.y - cupBottom.height / 2 && b.x > cupBottom.x - cupBottom.width / 2 && b.x < cupBottom.x + cupBottom.width / 2 && b.y < cupBottom.y + cupBottom.height / 2) { // Collected! collectedCount++; scoreTxt.setText('Balls: ' + collectedCount); b.deactivate(); b.destroy(); balls.splice(i, 1); continue; } // Remove balls that fall off screen if (b.y - b.radius > GAME_HEIGHT + 100) { b.deactivate(); b.destroy(); balls.splice(i, 1); continue; } // Ball-wall collision: bounce off left and right borders var leftWallX = leftBorder.x + leftBorder.width / 2; var rightWallX = rightBorder.x - rightBorder.width / 2; if (b.x - b.radius < leftWallX) { b.x = leftWallX + b.radius; b.vx = Math.abs(b.vx) * 0.8 + Math.random() * 0.5; // bounce right, dampen } if (b.x + b.radius > rightWallX) { b.x = rightWallX - b.radius; b.vx = -Math.abs(b.vx) * 0.8 - Math.random() * 0.5; // bounce left, dampen } } // End condition: all balls dropped and none left in play if (dropping && ballsToDrop === 0 && balls.length === 0) { dropping = false; // At end, show only collected balls as final score scoreTxt.setText('Balls: ' + collectedCount); infoTxt.setText('Final: ' + collectedCount + ' balls!\nTap to play again'); // Show game over (will reset game) LK.setScore(collectedCount); LK.showGameOver(); } }; // Reset state on game restart game.on('reset', function () { // Remove all balls for (var i = 0; i < balls.length; i++) { balls[i].destroy(); } balls = []; gatePassed = {}; ballIdCounter = 0; collectedCount = 0; dropping = false; ballsToDrop = 0; dropTimer = 0; scoreTxt.setText('Balls: 0'); infoTxt.setText('Drag cup & tap to drop balls!'); // Reset cup positions cupTop.x = GAME_WIDTH / 2; cupTop.y = 250; cupBottom.x = GAME_WIDTH / 2; cupBottom.y = GAME_HEIGHT - 180; // Remove old gates from game for (var i = 0; i < gates.length; i++) { gates[i].destroy(); } gates = []; // Randomize new gates GATE_CONFIGS = randomizeGates(); // Create new gates for (var i = 0; i < GATE_CONFIGS.length; i++) { var g = new Gate(); g.init(GATE_CONFIGS[i].type, GATE_CONFIGS[i].value); g.x = GAME_WIDTH / 2; g.y = GATE_CONFIGS[i].y; // Add movement state g.moveDir = Math.random() < 0.5 ? 1 : -1; // 1:right, -1:left g.moveSpeed = 4 + Math.random() * 2; // px per frame g.lastX = g.x; game.addChild(g); gates.push(g); } });
/****
* 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.active = true;
self.update = function () {
if (!self.active) return;
// Simple gravity
self.vy += 0.5; // slower gravity
self.x += self.vx;
self.y += self.vy;
};
// Used to mark as collected or destroyed
self.deactivate = function () {
self.active = false;
};
return self;
});
// Bottom cup class (collector)
var CupBottom = Container.expand(function () {
var self = Container.call(this);
// Add border using a slightly larger box behind
var border = self.attachAsset('cupBottom', {
anchorX: 0.5,
anchorY: 0.5,
width: 2048 + 16,
height: 100 + 16,
color: 0x222222
});
var cupGfx = self.attachAsset('cupBottom', {
anchorX: 0.5,
anchorY: 0.5
});
self.setChildIndex(border, 0);
self.width = cupGfx.width;
self.height = cupGfx.height;
return self;
});
// Top cup class (movable)
var CupTop = Container.expand(function () {
var self = Container.call(this);
var cupGfx = self.attachAsset('cupTop', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = cupGfx.width;
self.height = cupGfx.height;
// Add border using a slightly larger box behind
var border = self.attachAsset('cupTop', {
anchorX: 0.5,
anchorY: 0.5,
width: cupGfx.width + 12,
height: cupGfx.height + 12,
color: 0x222222
});
self.setChildIndex(border, 0);
return self;
});
// Gate class (multiplier or divider)
var Gate = Container.expand(function () {
var self = Container.call(this);
self.type = 'mult'; // 'mult' or 'div'
self.value = 2; // e.g. 2x or /2
self.width = 180;
self.height = 40;
self.gfx = null;
self.label = null;
self.leftBorder = null;
self.rightBorder = null;
self.init = function (type, value) {
self.type = type;
self.value = value;
if (type === 'mult') {
self.gfx = self.attachAsset('gateMult', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'div') {
self.gfx = self.attachAsset('gateDiv', {
anchorX: 0.5,
anchorY: 0.5
});
} else if (type === 'reverse') {
self.gfx = self.attachAsset('gateDiv', {
anchorX: 0.5,
anchorY: 0.5,
color: 0x3498db // blue for reverse
});
} else if (type === 'speed') {
self.gfx = self.attachAsset('gateMult', {
anchorX: 0.5,
anchorY: 0.5,
color: 0x27ae60 // green for speed
});
} else if (type === 'bounce') {
self.gfx = self.attachAsset('gateMult', {
anchorX: 0.5,
anchorY: 0.5,
color: 0xe74c3c // red for bounce
});
}
var txt = '';
if (type === 'mult') {
txt = 'x' + value;
} else if (type === 'div') {
txt = '÷' + value;
} else if (type === 'reverse') {
txt = 'REV';
} else if (type === 'speed') {
txt = value < 1 ? value + 'x SLOW' : value + 'x FAST';
} else if (type === 'bounce') {
txt = 'BOUNCE';
}
self.label = new Text2(txt, {
size: 60,
fill: 0xFFFFFF
});
self.label.anchor.set(0.5, 0.5);
self.addChild(self.label);
// Add physical vertical borders to the sides of the gate
// Use a tall, thin box for each border
var borderWidth = 32;
var borderHeight = self.gfx.height + 60;
self.leftBorder = self.attachAsset('cupTop', {
anchorX: 0.5,
anchorY: 0.5,
width: borderWidth,
height: borderHeight,
color: 0x222222
});
self.rightBorder = self.attachAsset('cupTop', {
anchorX: 0.5,
anchorY: 0.5,
width: borderWidth,
height: borderHeight,
color: 0x222222
});
// Position borders at left/right edge of gate
self.leftBorder.x = -self.gfx.width / 2 - borderWidth / 2 + 2;
self.leftBorder.y = 0;
self.rightBorder.x = self.gfx.width / 2 + borderWidth / 2 - 2;
self.rightBorder.y = 0;
// Ensure borders are behind the gate graphic
self.setChildIndex(self.leftBorder, 0);
self.setChildIndex(self.rightBorder, 0);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222b3a
});
/****
* Game Code
****/
// Game constants
// Top cup (where balls are dropped from)
// Bottom cup (where balls are collected)
// Ball
// Multiplier gate
// Divider gate
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var BALL_DROP_COUNT = 10;
var BALL_DROP_INTERVAL = 8; // frames between balls
// Randomize gate positions and types
var GATE_COUNT = 6;
var GATE_MIN_Y = 500;
var GATE_MAX_Y = GAME_HEIGHT - 600;
var GATE_SPACING_MIN = 300;
var GATE_SPACING_MAX = 500;
var GATE_TYPES = [{
type: 'mult',
values: [2, 3, 4, 5]
}, {
type: 'div',
values: [2]
}, {
type: 'reverse',
values: [1] // no value needed, but keep for consistency
}, {
type: 'speed',
values: [0.5, 1.5, 2] // 0.5x (slower), 1.5x, 2x (faster)
}, {
type: 'bounce',
values: [1] // no value needed, but keep for consistency
}];
function randomizeGates() {
var configs = [];
var lastY = GATE_MIN_Y - 200;
for (var i = 0; i < GATE_COUNT; i++) {
// Randomly pick type
var typeIdx = Math.floor(Math.random() * GATE_TYPES.length);
var typeObj = GATE_TYPES[typeIdx];
var value = typeObj.values[Math.floor(Math.random() * typeObj.values.length)];
// Randomly space Y
var spacing = GATE_SPACING_MIN + Math.floor(Math.random() * (GATE_SPACING_MAX - GATE_SPACING_MIN));
var y = lastY + spacing;
if (y > GATE_MAX_Y) y = GATE_MAX_Y;
lastY = y;
configs.push({
type: typeObj.type,
value: value,
y: y
});
}
return configs;
}
var GATE_CONFIGS = randomizeGates();
// Game state
var cupTop, cupBottom;
var balls = [];
var gates = [];
var dropping = false;
var dropTimer = 0;
var ballsToDrop = 0;
var scoreTxt, infoTxt;
var collectedCount = 0;
var gatePassed = {}; // ball.id -> {gateIndex: true}
var ballIdCounter = 0;
var dragNode = null;
// Add border walls to constrain balls
var leftBorder = game.attachAsset('cupTop', {
anchorX: 0.5,
anchorY: 0.5,
width: 60,
height: GAME_HEIGHT - 200,
color: 0x222222
});
leftBorder.x = 30;
leftBorder.y = GAME_HEIGHT / 2;
game.addChild(leftBorder);
var rightBorder = game.attachAsset('cupTop', {
anchorX: 0.5,
anchorY: 0.5,
width: 60,
height: GAME_HEIGHT - 200,
color: 0x222222
});
rightBorder.x = GAME_WIDTH - 30;
rightBorder.y = GAME_HEIGHT / 2;
game.addChild(rightBorder);
// Create top cup
cupTop = new CupTop();
cupTop.x = GAME_WIDTH / 2;
cupTop.y = 250;
game.addChild(cupTop);
// Create bottom cup
cupBottom = new CupBottom();
cupBottom.x = GAME_WIDTH / 2;
cupBottom.y = GAME_HEIGHT - 180;
game.addChild(cupBottom);
// Create gates
for (var i = 0; i < GATE_CONFIGS.length; i++) {
var g = new Gate();
g.init(GATE_CONFIGS[i].type, GATE_CONFIGS[i].value);
g.x = GAME_WIDTH / 2;
g.y = GATE_CONFIGS[i].y;
// Add movement state
g.moveDir = Math.random() < 0.5 ? 1 : -1; // 1:right, -1:left
g.moveSpeed = 4 + Math.random() * 2; // px per frame
g.lastX = g.x;
game.addChild(g);
gates.push(g);
}
// Score text (balls collected)
scoreTxt = new Text2('Balls: 0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Info text (instructions)
infoTxt = new Text2('Drag cup & tap to drop balls!', {
size: 60,
fill: 0xFFFFFF
});
infoTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(infoTxt);
// Position GUI elements
scoreTxt.y = 120;
infoTxt.y = 260;
// Helper: spawn a single ball at cupTop
function spawnBall(x, y, vx, vy) {
var b = new Ball();
b.x = x;
b.y = y;
b.vx = vx || 0;
b.vy = vy || 0;
b.id = ++ballIdCounter;
balls.push(b);
game.addChild(b);
gatePassed[b.id] = {};
return b;
}
// Start dropping balls
function startDrop() {
if (dropping) return;
dropping = true;
ballsToDrop = BALL_DROP_COUNT;
dropTimer = 0;
infoTxt.setText('Balls dropping...');
}
// Handle drag/move of cup
function handleMove(x, y, obj) {
if (dragNode) {
// Clamp cup within screen, avoid top left 100x100
var minX = 150 + dragNode.width / 2;
var maxX = GAME_WIDTH - dragNode.width / 2 - 50;
dragNode.x = Math.max(minX, Math.min(maxX, x));
}
}
// Touch/drag events
game.down = function (x, y, obj) {
// If touch is on cupTop, start drag
var local = cupTop.toLocal(game.toGlobal({
x: x,
y: y
}));
if (local.x > -cupTop.width / 2 && local.x < cupTop.width / 2 && local.y > -cupTop.height / 2 && local.y < cupTop.height / 2) {
dragNode = cupTop;
}
};
game.move = function (x, y, obj) {
handleMove(x, y, obj);
};
game.up = function (x, y, obj) {
dragNode = null;
// If not dragging, and not dropping, start drop on tap
if (!dropping) {
startDrop();
}
};
// Main update loop
game.update = function () {
// Handle dropping balls
if (dropping && ballsToDrop > 0) {
dropTimer--;
if (dropTimer <= 0) {
// Drop a ball from cupTop center, with smaller random vx and lower initial vy
var vx = (Math.random() - 0.5) * 3;
var b = spawnBall(cupTop.x, cupTop.y + cupTop.height / 2 + 10, vx, 1);
ballsToDrop--;
dropTimer = BALL_DROP_INTERVAL;
}
}
// Move gates side to side and prevent overlap
for (var i = 0; i < gates.length; i++) {
var g = gates[i];
g.lastX = g.x;
// Move
g.x += g.moveDir * g.moveSpeed;
// Clamp to screen
var minX = g.gfx.width / 2 + 60;
var maxX = GAME_WIDTH - g.gfx.width / 2 - 60;
if (g.x < minX) {
g.x = minX;
g.moveDir = 1;
}
if (g.x > maxX) {
g.x = maxX;
g.moveDir = -1;
}
}
// Prevent overlap between gates (simple separation, left-to-right)
for (var i = 0; i < gates.length; i++) {
for (var j = i + 1; j < gates.length; j++) {
var g1 = gates[i];
var g2 = gates[j];
// Only check if y overlap (vertical proximity)
if (Math.abs(g1.y - g2.y) < 120) {
var minDist = (g1.gfx.width + g2.gfx.width) / 2 + 40;
var dx = g2.x - g1.x;
if (Math.abs(dx) < minDist) {
// Push apart
var overlap = minDist - Math.abs(dx);
var push = overlap / 2;
if (dx === 0) dx = (Math.random() < 0.5 ? -1 : 1) * 0.1; // avoid zero
var dir = dx > 0 ? 1 : -1;
g1.x -= push * dir;
g2.x += push * dir;
// Clamp after push
var minX1 = g1.gfx.width / 2 + 60;
var maxX1 = GAME_WIDTH - g1.gfx.width / 2 - 60;
var minX2 = g2.gfx.width / 2 + 60;
var maxX2 = GAME_WIDTH - g2.gfx.width / 2 - 60;
g1.x = Math.max(minX1, Math.min(maxX1, g1.x));
g2.x = Math.max(minX2, Math.min(maxX2, g2.x));
// Reverse directions if they crash
g1.moveDir *= -1;
g2.moveDir *= -1;
}
}
}
}
// Show current number of balls in play (real time, not just collected)
scoreTxt.setText('Balls: ' + (collectedCount + balls.length));
for (var i = balls.length - 1; i >= 0; i--) {
var b = balls[i];
b.update();
// Check for gate collisions
for (var gidx = 0; gidx < gates.length; gidx++) {
var g = gates[gidx];
// Only process if not already passed
if (!gatePassed[b.id][gidx]) {
// Simple AABB collision
if (b.x + b.radius > g.x - g.width / 2 && b.x - b.radius < g.x + g.width / 2 && b.y + b.radius > g.y - g.height / 2 && b.y - b.radius < g.y + g.height / 2) {
// Mark as passed
gatePassed[b.id][gidx] = true;
// Apply gate effect
if (g.type === 'mult') {
// Multiply: spawn (value-1) new balls at same position
for (var m = 1; m < g.value; m++) {
// Spread balls more: wider vx, add a little vy randomness
var spreadVx = (Math.random() - 0.5) * 14;
var spreadVy = b.vy * 0.7 + (Math.random() - 0.5) * 2;
var nb = spawnBall(b.x, b.y, spreadVx, spreadVy);
// Copy gatePassed so new balls don't re-trigger previous gates
for (var k in gatePassed[b.id]) {
gatePassed[nb.id][k] = true;
}
}
} else if (g.type === 'div') {
// Divide: only keep 1/value balls, remove others
// Remove this ball if Math.random() > 1/value
if (Math.random() > 1 / g.value) {
b.deactivate();
}
} else if (g.type === 'reverse') {
// Reverse: flip vx and vy
b.vx = -b.vx;
b.vy = -Math.abs(b.vy) * 0.7; // send upward, reduce speed
} else if (g.type === 'speed') {
// Speed: multiply vy and vx by value
b.vx *= g.value;
b.vy *= g.value;
} else if (g.type === 'bounce') {
// Bounce: invert vy, reduce speed, add a little random
b.vy = -Math.abs(b.vy) * (0.7 + Math.random() * 0.2);
b.vx += (Math.random() - 0.5) * 6;
}
}
}
}
// Remove deactivated balls
if (!b.active) {
b.destroy();
balls.splice(i, 1);
continue;
}
// Check for collection in bottom cup
if (b.y + b.radius > cupBottom.y - cupBottom.height / 2 && b.x > cupBottom.x - cupBottom.width / 2 && b.x < cupBottom.x + cupBottom.width / 2 && b.y < cupBottom.y + cupBottom.height / 2) {
// Collected!
collectedCount++;
scoreTxt.setText('Balls: ' + collectedCount);
b.deactivate();
b.destroy();
balls.splice(i, 1);
continue;
}
// Remove balls that fall off screen
if (b.y - b.radius > GAME_HEIGHT + 100) {
b.deactivate();
b.destroy();
balls.splice(i, 1);
continue;
}
// Ball-wall collision: bounce off left and right borders
var leftWallX = leftBorder.x + leftBorder.width / 2;
var rightWallX = rightBorder.x - rightBorder.width / 2;
if (b.x - b.radius < leftWallX) {
b.x = leftWallX + b.radius;
b.vx = Math.abs(b.vx) * 0.8 + Math.random() * 0.5; // bounce right, dampen
}
if (b.x + b.radius > rightWallX) {
b.x = rightWallX - b.radius;
b.vx = -Math.abs(b.vx) * 0.8 - Math.random() * 0.5; // bounce left, dampen
}
}
// End condition: all balls dropped and none left in play
if (dropping && ballsToDrop === 0 && balls.length === 0) {
dropping = false;
// At end, show only collected balls as final score
scoreTxt.setText('Balls: ' + collectedCount);
infoTxt.setText('Final: ' + collectedCount + ' balls!\nTap to play again');
// Show game over (will reset game)
LK.setScore(collectedCount);
LK.showGameOver();
}
};
// Reset state on game restart
game.on('reset', function () {
// Remove all balls
for (var i = 0; i < balls.length; i++) {
balls[i].destroy();
}
balls = [];
gatePassed = {};
ballIdCounter = 0;
collectedCount = 0;
dropping = false;
ballsToDrop = 0;
dropTimer = 0;
scoreTxt.setText('Balls: 0');
infoTxt.setText('Drag cup & tap to drop balls!');
// Reset cup positions
cupTop.x = GAME_WIDTH / 2;
cupTop.y = 250;
cupBottom.x = GAME_WIDTH / 2;
cupBottom.y = GAME_HEIGHT - 180;
// Remove old gates from game
for (var i = 0; i < gates.length; i++) {
gates[i].destroy();
}
gates = [];
// Randomize new gates
GATE_CONFIGS = randomizeGates();
// Create new gates
for (var i = 0; i < GATE_CONFIGS.length; i++) {
var g = new Gate();
g.init(GATE_CONFIGS[i].type, GATE_CONFIGS[i].value);
g.x = GAME_WIDTH / 2;
g.y = GATE_CONFIGS[i].y;
// Add movement state
g.moveDir = Math.random() < 0.5 ? 1 : -1; // 1:right, -1:left
g.moveSpeed = 4 + Math.random() * 2; // px per frame
g.lastX = g.x;
game.addChild(g);
gates.push(g);
}
});