/**** 
* Plugins
****/ 
var tween = LK.import("@upit/tween.v1");
/**** 
* Classes
****/ 
var Ball = Container.expand(function (value) {
	var self = Container.call(this);
	self.value = value || 2;
	self.velocityX = 0;
	self.velocityY = 0;
	self.gravity = 0.8;
	self.bounce = 0.4;
	self.friction = 0.98;
	// Set radius based on ball value for proper physics - reduced for tighter packing
	var radiusMap = {
		2: 90,
		4: 95,
		8: 105,
		16: 115,
		32: 125,
		64: 135,
		128: 145,
		256: 155,
		512: 165,
		1024: 175,
		2048: 185
	};
	self.radius = radiusMap[self.value] || 60;
	self.isStatic = false;
	self.mergeTimer = 0;
	self.hasBeenMerged = false;
	var ballAsset;
	try {
		ballAsset = self.attachAsset('ball' + self.value, {
			anchorX: 0.5,
			anchorY: 0.5
		});
	} catch (e) {
		// Fallback for 2048 ball if image asset fails
		if (self.value === 2048) {
			ballAsset = self.attachAsset('ball2048top', {
				anchorX: 0.5,
				anchorY: 0.5
			});
		} else {
			throw e;
		}
	}
	var valueText = new Text2(self.value.toString(), {
		size: 56,
		fill: 0xFFFFFF
	});
	valueText.anchor.set(0.5, 0.5);
	self.addChild(valueText);
	self.update = function () {
		if (self.hasBeenMerged) return;
		if (self.mergeTimer > 0) {
			self.mergeTimer--;
			return;
		}
		// Apply physics only if not static
		if (!self.isStatic) {
			// Store previous position for continuous collision detection
			var prevX = self.x;
			var prevY = self.y;
			// Apply gravity
			self.velocityY += self.gravity;
			// Calculate intended new position
			var newX = self.x + self.velocityX;
			var newY = self.y + self.velocityY;
			// Continuous collision detection - check path from current to intended position
			var stepCount = Math.max(1, Math.ceil(Math.abs(self.velocityX) + Math.abs(self.velocityY)) / (self.radius * 0.5));
			var stepX = (newX - self.x) / stepCount;
			var stepY = (newY - self.y) / stepCount;
			var collisionOccurred = false;
			// Check each step along the movement path
			for (var step = 1; step <= stepCount && !collisionOccurred; step++) {
				var testX = self.x + stepX * step;
				var testY = self.y + stepY * step;
				// Test collision with other balls
				for (var i = 0; i < balls.length; i++) {
					var otherBall = balls[i];
					if (otherBall === self || otherBall.hasBeenMerged || otherBall.mergeTimer > 0) continue;
					var dx = otherBall.x - testX;
					var dy = otherBall.y - testY;
					var distance = Math.sqrt(dx * dx + dy * dy);
					var minDistance = (self.radius + otherBall.radius) * 0.94; // Slightly looser packing
					if (distance < minDistance) {
						// Collision detected - stop at safe position
						var safeDistance = minDistance + 1; // Add smaller buffer for tighter packing
						if (distance > 0) {
							var normalX = dx / distance;
							var normalY = dy / distance;
							self.x = otherBall.x - normalX * safeDistance;
							self.y = otherBall.y - normalY * safeDistance;
							// Check for merge first - increased sensitivity for same-numbered balls
							var mergeDistance = self.value === otherBall.value ? minDistance * 1.4 : minDistance;
							if (self.value === otherBall.value && self.mergeTimer === 0 && otherBall.mergeTimer === 0 && distance < mergeDistance) {
								// Special case: when two 2048 balls merge, they explode and disappear
								if (self.value === 2048) {
									// Create explosion effect for both 2048 balls
									tween(self, {
										scaleX: 3.0,
										scaleY: 3.0,
										alpha: 0
									}, {
										duration: 600,
										easing: tween.easeOut
									});
									tween(otherBall, {
										scaleX: 3.0,
										scaleY: 3.0,
										alpha: 0
									}, {
										duration: 600,
										easing: tween.easeOut
									});
									// Mark both balls for removal
									self.hasBeenMerged = true;
									otherBall.hasBeenMerged = true;
									LK.getSound('merge').play();
									if (scoringActive) {
										LK.setScore(LK.getScore() + 1);
										scoreText.setText(LK.getScore());
									}
									return;
								}
								var newValue = self.value * 2;
								if (newValue <= 2048) {
									var newBall = new Ball(newValue);
									newBall.x = (self.x + otherBall.x) / 2;
									newBall.y = (self.y + otherBall.y) / 2;
									newBall.velocityX = (self.velocityX + otherBall.velocityX) / 2;
									newBall.velocityY = (self.velocityY + otherBall.velocityY) / 2;
									newBall.mergeTimer = 10;
									// Add merge animation - scale and pulse effect
									newBall.scaleX = 0.2; // Start very small
									newBall.scaleY = 0.2; // Start very small
									newBall.alpha = 0.7; // Start semi-transparent
									tween(newBall, {
										scaleX: 1.3,
										scaleY: 1.3,
										alpha: 1
									}, {
										duration: 200,
										easing: tween.easeOut,
										onFinish: function onFinish() {
											// Pulse back to normal size
											tween(newBall, {
												scaleX: 1,
												scaleY: 1
											}, {
												duration: 150,
												easing: tween.easeInOut
											});
										}
									});
									// Animate merging balls with shrink and fade
									tween(self, {
										scaleX: 0,
										scaleY: 0,
										alpha: 0
									}, {
										duration: 250,
										easing: tween.easeIn
									});
									tween(otherBall, {
										scaleX: 0,
										scaleY: 0,
										alpha: 0
									}, {
										duration: 250,
										easing: tween.easeIn
									});
									balls.push(newBall);
									gameArea.addChild(newBall);
									// Mark for removal
									self.hasBeenMerged = true;
									otherBall.hasBeenMerged = true;
									LK.getSound('merge').play();
									if (scoringActive) {
										LK.setScore(LK.getScore() + 1);
										scoreText.setText(LK.getScore());
									}
									// No win condition - game continues even after reaching 2048
									return;
								}
							}
							// Enhanced collision response for better stacking
							var relativeVelX = self.velocityX - otherBall.velocityX;
							var relativeVelY = self.velocityY - otherBall.velocityY;
							var relativeSpeed = relativeVelX * normalX + relativeVelY * normalY;
							if (relativeSpeed > 0) {
								// Calculate mass-based collision response (assume equal mass)
								var restitution = 0.3; // Lower restitution for more stable stacking
								var impulse = (1 + restitution) * relativeSpeed * 0.5;
								self.velocityX -= impulse * normalX;
								self.velocityY -= impulse * normalY;
								otherBall.velocityX += impulse * normalX;
								otherBall.velocityY += impulse * normalY;
								// Apply different friction based on collision angle
								var collisionAngle = Math.atan2(normalY, normalX);
								var frictionFactor = Math.abs(Math.sin(collisionAngle)) * 0.8 + 0.2;
								self.velocityX *= self.friction * frictionFactor;
								self.velocityY *= self.friction * frictionFactor;
								otherBall.velocityX *= otherBall.friction * frictionFactor;
								otherBall.velocityY *= otherBall.friction * frictionFactor;
								// Enhanced static detection for stacking
								if (Math.abs(self.velocityY) < 0.5 && Math.abs(self.velocityX) < 0.5) {
									// Check if ball is supported (has another ball below it or touching ground)
									var supportFound = false;
									var localBottom = gameAreaHeight - 20;
									// Check ground support
									if (self.y + self.radius >= localBottom - 5) {
										supportFound = true;
									} else {
										// Check support from other balls
										for (var j = 0; j < balls.length; j++) {
											var supportBall = balls[j];
											if (supportBall === self || supportBall.hasBeenMerged) continue;
											var supportDx = supportBall.x - self.x;
											var supportDy = supportBall.y - self.y;
											var supportDistance = Math.sqrt(supportDx * supportDx + supportDy * supportDy);
											// Check if this ball is resting on another ball (other ball is below)
											if (supportDistance < (self.radius + supportBall.radius) * 1.1 && supportDy > 0) {
												supportFound = true;
												break;
											}
										}
									}
									if (supportFound) {
										self.isStatic = true;
										self.velocityX = 0;
										self.velocityY = 0;
									}
								}
							}
						}
						collisionOccurred = true;
						break;
					}
				}
			}
			// If no collision occurred, move to intended position
			if (!collisionOccurred) {
				self.x = newX;
				self.y = newY;
			}
			// Boundary collisions after position update
			var localWidth = gameAreaRight - gameAreaLeft;
			var localBottom = gameAreaHeight - 20; // gameAreaHeight minus bottom wall height
			// Ground collision
			if (self.y + self.radius > localBottom) {
				self.y = localBottom - self.radius;
				self.velocityY *= -self.bounce;
				self.velocityX *= self.friction;
				if (Math.abs(self.velocityY) < 1) {
					self.velocityY = 0;
					self.isStatic = true;
				}
			}
			// Side walls collision
			if (self.x - self.radius < 0) {
				self.x = self.radius;
				self.velocityX *= -self.bounce;
			}
			if (self.x + self.radius > localWidth) {
				self.x = localWidth - self.radius;
				self.velocityX *= -self.bounce;
			}
		}
		// Enhanced merge detection pass - check for nearby same-numbered balls
		for (var i = 0; i < balls.length; i++) {
			var otherBall = balls[i];
			if (otherBall === self || otherBall.hasBeenMerged || otherBall.mergeTimer > 0 || self.mergeTimer > 0) continue;
			var dx = otherBall.x - self.x;
			var dy = otherBall.y - self.y;
			var distance = Math.sqrt(dx * dx + dy * dy);
			var minDistance = (self.radius + otherBall.radius) * 0.94; // Slightly looser packing
			// Enhanced merge sensitivity for same-numbered balls
			if (self.value === otherBall.value) {
				var mergeTriggerDistance = minDistance * 1.4; // Increased sensitivity to compensate for reduced minDistance
				if (distance < mergeTriggerDistance) {
					// Special case: when two 2048 balls merge, they explode and disappear
					if (self.value === 2048) {
						// Create explosion effect for both 2048 balls
						tween(self, {
							scaleX: 3.0,
							scaleY: 3.0,
							alpha: 0
						}, {
							duration: 600,
							easing: tween.easeOut
						});
						tween(otherBall, {
							scaleX: 3.0,
							scaleY: 3.0,
							alpha: 0
						}, {
							duration: 600,
							easing: tween.easeOut
						});
						// Mark both balls for removal
						self.hasBeenMerged = true;
						otherBall.hasBeenMerged = true;
						LK.getSound('merge').play();
						if (scoringActive) {
							LK.setScore(LK.getScore() + 1);
							scoreText.setText(LK.getScore());
						}
						return;
					}
					var newValue = self.value * 2;
					if (newValue <= 2048) {
						var newBall = new Ball(newValue);
						newBall.x = (self.x + otherBall.x) / 2;
						newBall.y = (self.y + otherBall.y) / 2;
						newBall.velocityX = (self.velocityX + otherBall.velocityX) / 2;
						newBall.velocityY = (self.velocityY + otherBall.velocityY) / 2;
						newBall.mergeTimer = 10;
						// Add merge animation - scale and pulse effect
						newBall.scaleX = 0.2; // Start very small
						newBall.scaleY = 0.2; // Start very small
						newBall.alpha = 0.7; // Start semi-transparent
						tween(newBall, {
							scaleX: 1.3,
							scaleY: 1.3,
							alpha: 1
						}, {
							duration: 200,
							easing: tween.easeOut,
							onFinish: function onFinish() {
								// Pulse back to normal size
								tween(newBall, {
									scaleX: 1,
									scaleY: 1
								}, {
									duration: 150,
									easing: tween.easeInOut
								});
							}
						});
						// Animate merging balls with shrink and fade
						tween(self, {
							scaleX: 0,
							scaleY: 0,
							alpha: 0
						}, {
							duration: 250,
							easing: tween.easeIn
						});
						tween(otherBall, {
							scaleX: 0,
							scaleY: 0,
							alpha: 0
						}, {
							duration: 250,
							easing: tween.easeIn
						});
						balls.push(newBall);
						gameArea.addChild(newBall);
						// Mark for removal
						self.hasBeenMerged = true;
						otherBall.hasBeenMerged = true;
						LK.getSound('merge').play();
						if (scoringActive) {
							LK.setScore(LK.getScore() + 1);
							scoreText.setText(LK.getScore());
						}
						// No win condition - game continues even after reaching 2048
						return;
					}
				}
			}
			// Enhanced separation for stable stacking
			if (distance < minDistance && distance > 0) {
				var overlap = minDistance - distance;
				var normalX = dx / distance;
				var normalY = dy / distance;
				// Different separation strategies based on ball positions
				var separationForce = overlap * 0.5;
				// If one ball is static and the other is moving, adjust separation
				if (self.isStatic && !otherBall.isStatic) {
					// Static ball moves less
					self.x -= normalX * separationForce * 0.2;
					self.y -= normalY * separationForce * 0.2;
					otherBall.x += normalX * separationForce * 0.8;
					otherBall.y += normalY * separationForce * 0.8;
				} else if (!self.isStatic && otherBall.isStatic) {
					// Other ball is static
					self.x -= normalX * separationForce * 0.8;
					self.y -= normalY * separationForce * 0.8;
					otherBall.x += normalX * separationForce * 0.2;
					otherBall.y += normalY * separationForce * 0.2;
				} else {
					// Both moving or both static - equal separation
					self.x -= normalX * separationForce;
					self.y -= normalY * separationForce;
					otherBall.x += normalX * separationForce;
					otherBall.y += normalY * separationForce;
				}
				// Ensure balls stay within bounds after separation
				var localWidth = gameAreaRight - gameAreaLeft;
				var localBottom = gameAreaHeight - 20; // gameAreaHeight minus bottom wall height
				self.x = Math.max(self.radius, Math.min(localWidth - self.radius, self.x));
				self.y = Math.max(self.radius, Math.min(localBottom - self.radius, self.y));
				// Re-check static state after separation
				if (self.isStatic && (Math.abs(self.x - (self.x - normalX * separationForce)) > 1 || Math.abs(self.y - (self.y - normalY * separationForce)) > 1)) {
					self.isStatic = false; // Ball was moved significantly, make it dynamic again
				}
			}
		}
		// Periodic re-evaluation of static state for better stacking
		if (LK.ticks % 10 === 0 && !self.hasBeenMerged) {
			// Check every 10 frames
			if (!self.isStatic && Math.abs(self.velocityX) < 0.3 && Math.abs(self.velocityY) < 0.3) {
				// Check if ball should become static
				var supportFound = false;
				var localBottom = gameAreaHeight - 20;
				// Check ground support
				if (self.y + self.radius >= localBottom - 2) {
					supportFound = true;
				} else {
					// Check support from other static balls
					for (var k = 0; k < balls.length; k++) {
						var checkBall = balls[k];
						if (checkBall === self || checkBall.hasBeenMerged || !checkBall.isStatic) continue;
						var checkDx = checkBall.x - self.x;
						var checkDy = checkBall.y - self.y;
						var checkDistance = Math.sqrt(checkDx * checkDx + checkDy * checkDy);
						// Ball is resting on another static ball
						if (checkDistance < (self.radius + checkBall.radius) * 1.05 && checkDy > 0) {
							supportFound = true;
							break;
						}
					}
				}
				if (supportFound) {
					self.isStatic = true;
					self.velocityX = 0;
					self.velocityY = 0;
				}
			} else if (self.isStatic) {
				// Check if static ball should become dynamic (lost support)
				var stillSupported = false;
				var localBottom = gameAreaHeight - 20;
				// Check ground support
				if (self.y + self.radius >= localBottom - 2) {
					stillSupported = true;
				} else {
					// Check support from other balls
					for (var k = 0; k < balls.length; k++) {
						var supportBall = balls[k];
						if (supportBall === self || supportBall.hasBeenMerged) continue;
						var supportDx = supportBall.x - self.x;
						var supportDy = supportBall.y - self.y;
						var supportDistance = Math.sqrt(supportDx * supportDx + supportDy * supportDy);
						if (supportDistance < (self.radius + supportBall.radius) * 1.05 && supportDy > 0) {
							stillSupported = true;
							break;
						}
					}
				}
				if (!stillSupported) {
					self.isStatic = false; // Ball lost support, make it fall
				}
			}
		}
	};
	return self;
});
/**** 
* Initialize Game
****/ 
var game = new LK.Game({
	backgroundColor: 0xf5f5dc
});
/**** 
* Game Code
****/ 
var balls = [];
var nextBallValue = 2;
var gameAreaLeft = 0;
var gameAreaRight = 2048;
var gameAreaTop = 0;
var gameAreaBottom = 2732;
var dropCooldown = 0;
var gameAreaHeight = gameAreaBottom - gameAreaTop;
var dangerZoneImmunity = 300; // 5 seconds at 60fps
var scoringActive = false; // Flag to track when scoring should be active
// Create game area background
var gameAreaBg = game.addChild(LK.getAsset('gameArea', {
	anchorX: 0.5,
	anchorY: 0,
	x: 1024,
	y: gameAreaTop
}));
// Create game area container for balls
var gameArea = new Container();
gameArea.x = gameAreaLeft;
gameArea.y = gameAreaTop;
game.addChild(gameArea);
// Create visible walls
var leftWall = game.addChild(LK.getAsset('leftWall', {
	anchorX: 1,
	anchorY: 0,
	x: gameAreaLeft,
	y: gameAreaTop
}));
var rightWall = game.addChild(LK.getAsset('rightWall', {
	anchorX: 0,
	anchorY: 0,
	x: gameAreaRight,
	y: gameAreaTop
}));
var bottomWall = game.addChild(LK.getAsset('bottomWall', {
	anchorX: 0.5,
	anchorY: 0,
	x: 1024,
	y: gameAreaBottom
}));
// Create danger zone line (visible red line showing danger threshold)
var dangerZoneLine = game.addChild(LK.getAsset('gameOverLine', {
	anchorX: 0.5,
	anchorY: 0.5,
	x: 1024,
	y: gameAreaTop + 350,
	alpha: 0.8,
	visible: true
}));
// Create score display
var scoreText = new Text2('0', {
	size: 100,
	fill: 0xFFFFFF
});
scoreText.anchor.set(1, 0);
LK.gui.topRight.addChild(scoreText);
scoreText.x = -50; // Small offset from right edge
scoreText.y = 100;
// Create next ball preview
var nextBallPreview = LK.getAsset('ball' + nextBallValue, {
	anchorX: 0.5,
	anchorY: 1.0,
	x: 1024,
	y: gameAreaTop + 350,
	scaleX: 1.4,
	scaleY: 1.4
});
game.addChild(nextBallPreview);
// Create next ball value text
var nextBallText = new Text2(nextBallValue.toString(), {
	size: 48,
	fill: 0xFFFFFF
});
nextBallText.anchor.set(0.5, 0.5);
nextBallText.x = 1024;
nextBallText.y = gameAreaTop + 200;
game.addChild(nextBallText);
function getRandomNextBallValue() {
	var values = [2, 4, 8];
	return values[Math.floor(Math.random() * values.length)];
}
function updateNextBallPreview() {
	game.removeChild(nextBallPreview);
	game.removeChild(nextBallText);
	nextBallPreview = LK.getAsset('ball' + nextBallValue, {
		anchorX: 0.5,
		anchorY: 1.0,
		x: 1024,
		y: gameAreaTop + 350,
		scaleX: 1.4,
		scaleY: 1.4
	});
	game.addChild(nextBallPreview);
	nextBallText = new Text2(nextBallValue.toString(), {
		size: 48,
		fill: 0xFFFFFF
	});
	nextBallText.anchor.set(0.5, 0.5);
	nextBallText.x = 1024;
	nextBallText.y = gameAreaTop + 200;
	game.addChild(nextBallText);
}
function checkGameOver() {
	// Skip game over check during immunity period
	if (dangerZoneImmunity > 0) return;
	for (var i = 0; i < balls.length; i++) {
		// Check if ball touches the danger zone (first 350 pixels of game area)
		// Since balls are in gameArea container, we use local coordinates
		// Only trigger game over if ball is static (not actively falling) and in danger zone
		if (balls[i].y - balls[i].radius <= 350 && balls[i].isStatic) {
			LK.showGameOver();
			return;
		}
	}
}
function cleanupMergedBalls() {
	for (var i = balls.length - 1; i >= 0; i--) {
		if (balls[i].hasBeenMerged) {
			gameArea.removeChild(balls[i]);
			balls[i].destroy();
			balls.splice(i, 1);
		}
	}
}
// Create initial balls to fill container to the top
function createInitialBalls() {
	var localWidth = gameAreaRight - gameAreaLeft;
	var ballsPerRow = 7; // Fixed number of balls per row for consistent layout
	var rowBalls = []; // Track balls in current row for spacing calculation
	var rowHeights = []; // Track row heights for stacking
	var previousRowBalls = []; // Track balls from previous row for vertical spacing
	for (var row = 0; row < 12; row++) {
		rowBalls = []; // Reset for each row
		var maxRadiusInRow = 0; // Track largest ball in current row
		// Create balls for this row first to know their sizes
		var ballsData = [];
		for (var col = 0; col < ballsPerRow; col++) {
			var ballValue = getRandomNextBallValue();
			var radiusMap = {
				2: 90,
				4: 95,
				8: 105,
				16: 115,
				32: 125,
				64: 135,
				128: 145,
				256: 155,
				512: 165,
				1024: 175,
				2048: 185
			};
			var ballRadius = radiusMap[ballValue] || 60;
			ballsData.push({
				value: ballValue,
				radius: ballRadius
			});
			maxRadiusInRow = Math.max(maxRadiusInRow, ballRadius);
		}
		// Calculate X positions to make balls touch tightly horizontally - subtract overlap for tighter packing
		var xPositions = [];
		var currentX = 0;
		var overlapAmount = 8; // Slightly reduced overlap for looser horizontal packing
		for (var col = 0; col < ballsPerRow; col++) {
			if (col === 0) {
				// First ball starts at its radius
				currentX = ballsData[col].radius;
			} else {
				// Each subsequent ball is positioned closer than touching distance
				var prevRadius = ballsData[col - 1].radius;
				var currentRadius = ballsData[col].radius;
				currentX += prevRadius + currentRadius - overlapAmount; // Reduce spacing for tighter packing
			}
			xPositions.push(currentX);
		}
		// Center the entire row
		var totalRowWidth = xPositions[xPositions.length - 1] + ballsData[ballsPerRow - 1].radius;
		var offsetX = (localWidth - totalRowWidth) / 2;
		// Create and position the balls
		for (var col = 0; col < ballsPerRow; col++) {
			var initialBall = new Ball(ballsData[col].value);
			initialBall.x = xPositions[col] + offsetX;
			initialBall.velocityX = 0;
			initialBall.velocityY = 0;
			initialBall.isStatic = true; // Start as static for stable stacking
			rowBalls.push(initialBall);
			balls.push(initialBall);
			gameArea.addChild(initialBall);
		}
		// Calculate Y position to make balls touch tightly vertically with previous row
		var rowY;
		var verticalOverlap = 4; // Slightly reduced vertical overlap for looser packing
		if (row === 0) {
			// First row sits on bottom
			rowY = gameAreaHeight - 100 - maxRadiusInRow;
		} else {
			// For subsequent rows, find the minimum Y position with tighter packing
			rowY = gameAreaHeight; // Start with max possible Y
			for (var currentCol = 0; currentCol < rowBalls.length; currentCol++) {
				var currentBall = rowBalls[currentCol];
				var currentBallRadius = currentBall.radius;
				var minYForThisBall = gameAreaHeight - 100 - currentBallRadius; // Default to bottom
				// Check against all balls in previous row
				for (var prevCol = 0; prevCol < previousRowBalls.length; prevCol++) {
					var prevBall = previousRowBalls[prevCol];
					var dx = currentBall.x - prevBall.x;
					var dy = 0; // We'll calculate the required dy
					var horizontalDistance = Math.abs(dx);
					var requiredCenterDistance = currentBallRadius + prevBall.radius - verticalOverlap; // Tighter vertical packing
					// If balls are close enough horizontally to potentially touch
					if (horizontalDistance < requiredCenterDistance) {
						// Calculate the vertical distance needed for tighter packing
						var verticalDistance = Math.sqrt(Math.max(0, requiredCenterDistance * requiredCenterDistance - dx * dx));
						var requiredY = prevBall.y - verticalDistance;
						minYForThisBall = Math.min(minYForThisBall, requiredY);
					}
				}
				// The row Y is determined by the ball that needs to be highest (smallest Y)
				rowY = Math.min(rowY, minYForThisBall);
			}
		}
		// Apply the calculated Y position to all balls in this row
		for (var j = 0; j < rowBalls.length; j++) {
			rowBalls[j].y = rowY;
		}
		// Store this row's balls for next row calculation
		previousRowBalls = rowBalls.slice(); // Copy the array
	}
}
// Initialize the starting balls
createInitialBalls();
game.down = function (x, y, obj) {
	if (dropCooldown > 0) return;
	// Activate scoring system when first ball is dropped
	if (!scoringActive) {
		scoringActive = true;
	}
	// Constrain drop position to game area (convert to local coordinates)
	var localWidth = gameAreaRight - gameAreaLeft;
	var localX = x - gameAreaLeft;
	var dropX = Math.max(80, Math.min(localWidth - 80, localX));
	var newBall = new Ball(nextBallValue);
	newBall.x = dropX;
	newBall.y = 420; // Start below the danger zone (350px) plus ball radius (60px) plus buffer
	newBall.velocityX = 0;
	newBall.velocityY = 0;
	balls.push(newBall);
	gameArea.addChild(newBall);
	LK.getSound('drop').play();
	// Update next ball
	nextBallValue = getRandomNextBallValue();
	updateNextBallPreview();
	dropCooldown = 30; // 0.5 seconds at 60fps
};
game.update = function () {
	if (dropCooldown > 0) {
		dropCooldown--;
	}
	// Handle danger zone immunity countdown
	if (dangerZoneImmunity > 0) {
		dangerZoneImmunity--;
	}
	cleanupMergedBalls();
	checkGameOver();
	// Update score display
	scoreText.setText(LK.getScore());
}; /**** 
* Plugins
****/ 
var tween = LK.import("@upit/tween.v1");
/**** 
* Classes
****/ 
var Ball = Container.expand(function (value) {
	var self = Container.call(this);
	self.value = value || 2;
	self.velocityX = 0;
	self.velocityY = 0;
	self.gravity = 0.8;
	self.bounce = 0.4;
	self.friction = 0.98;
	// Set radius based on ball value for proper physics - reduced for tighter packing
	var radiusMap = {
		2: 90,
		4: 95,
		8: 105,
		16: 115,
		32: 125,
		64: 135,
		128: 145,
		256: 155,
		512: 165,
		1024: 175,
		2048: 185
	};
	self.radius = radiusMap[self.value] || 60;
	self.isStatic = false;
	self.mergeTimer = 0;
	self.hasBeenMerged = false;
	var ballAsset;
	try {
		ballAsset = self.attachAsset('ball' + self.value, {
			anchorX: 0.5,
			anchorY: 0.5
		});
	} catch (e) {
		// Fallback for 2048 ball if image asset fails
		if (self.value === 2048) {
			ballAsset = self.attachAsset('ball2048top', {
				anchorX: 0.5,
				anchorY: 0.5
			});
		} else {
			throw e;
		}
	}
	var valueText = new Text2(self.value.toString(), {
		size: 56,
		fill: 0xFFFFFF
	});
	valueText.anchor.set(0.5, 0.5);
	self.addChild(valueText);
	self.update = function () {
		if (self.hasBeenMerged) return;
		if (self.mergeTimer > 0) {
			self.mergeTimer--;
			return;
		}
		// Apply physics only if not static
		if (!self.isStatic) {
			// Store previous position for continuous collision detection
			var prevX = self.x;
			var prevY = self.y;
			// Apply gravity
			self.velocityY += self.gravity;
			// Calculate intended new position
			var newX = self.x + self.velocityX;
			var newY = self.y + self.velocityY;
			// Continuous collision detection - check path from current to intended position
			var stepCount = Math.max(1, Math.ceil(Math.abs(self.velocityX) + Math.abs(self.velocityY)) / (self.radius * 0.5));
			var stepX = (newX - self.x) / stepCount;
			var stepY = (newY - self.y) / stepCount;
			var collisionOccurred = false;
			// Check each step along the movement path
			for (var step = 1; step <= stepCount && !collisionOccurred; step++) {
				var testX = self.x + stepX * step;
				var testY = self.y + stepY * step;
				// Test collision with other balls
				for (var i = 0; i < balls.length; i++) {
					var otherBall = balls[i];
					if (otherBall === self || otherBall.hasBeenMerged || otherBall.mergeTimer > 0) continue;
					var dx = otherBall.x - testX;
					var dy = otherBall.y - testY;
					var distance = Math.sqrt(dx * dx + dy * dy);
					var minDistance = (self.radius + otherBall.radius) * 0.94; // Slightly looser packing
					if (distance < minDistance) {
						// Collision detected - stop at safe position
						var safeDistance = minDistance + 1; // Add smaller buffer for tighter packing
						if (distance > 0) {
							var normalX = dx / distance;
							var normalY = dy / distance;
							self.x = otherBall.x - normalX * safeDistance;
							self.y = otherBall.y - normalY * safeDistance;
							// Check for merge first - increased sensitivity for same-numbered balls
							var mergeDistance = self.value === otherBall.value ? minDistance * 1.4 : minDistance;
							if (self.value === otherBall.value && self.mergeTimer === 0 && otherBall.mergeTimer === 0 && distance < mergeDistance) {
								// Special case: when two 2048 balls merge, they explode and disappear
								if (self.value === 2048) {
									// Create explosion effect for both 2048 balls
									tween(self, {
										scaleX: 3.0,
										scaleY: 3.0,
										alpha: 0
									}, {
										duration: 600,
										easing: tween.easeOut
									});
									tween(otherBall, {
										scaleX: 3.0,
										scaleY: 3.0,
										alpha: 0
									}, {
										duration: 600,
										easing: tween.easeOut
									});
									// Mark both balls for removal
									self.hasBeenMerged = true;
									otherBall.hasBeenMerged = true;
									LK.getSound('merge').play();
									if (scoringActive) {
										LK.setScore(LK.getScore() + 1);
										scoreText.setText(LK.getScore());
									}
									return;
								}
								var newValue = self.value * 2;
								if (newValue <= 2048) {
									var newBall = new Ball(newValue);
									newBall.x = (self.x + otherBall.x) / 2;
									newBall.y = (self.y + otherBall.y) / 2;
									newBall.velocityX = (self.velocityX + otherBall.velocityX) / 2;
									newBall.velocityY = (self.velocityY + otherBall.velocityY) / 2;
									newBall.mergeTimer = 10;
									// Add merge animation - scale and pulse effect
									newBall.scaleX = 0.2; // Start very small
									newBall.scaleY = 0.2; // Start very small
									newBall.alpha = 0.7; // Start semi-transparent
									tween(newBall, {
										scaleX: 1.3,
										scaleY: 1.3,
										alpha: 1
									}, {
										duration: 200,
										easing: tween.easeOut,
										onFinish: function onFinish() {
											// Pulse back to normal size
											tween(newBall, {
												scaleX: 1,
												scaleY: 1
											}, {
												duration: 150,
												easing: tween.easeInOut
											});
										}
									});
									// Animate merging balls with shrink and fade
									tween(self, {
										scaleX: 0,
										scaleY: 0,
										alpha: 0
									}, {
										duration: 250,
										easing: tween.easeIn
									});
									tween(otherBall, {
										scaleX: 0,
										scaleY: 0,
										alpha: 0
									}, {
										duration: 250,
										easing: tween.easeIn
									});
									balls.push(newBall);
									gameArea.addChild(newBall);
									// Mark for removal
									self.hasBeenMerged = true;
									otherBall.hasBeenMerged = true;
									LK.getSound('merge').play();
									if (scoringActive) {
										LK.setScore(LK.getScore() + 1);
										scoreText.setText(LK.getScore());
									}
									// No win condition - game continues even after reaching 2048
									return;
								}
							}
							// Enhanced collision response for better stacking
							var relativeVelX = self.velocityX - otherBall.velocityX;
							var relativeVelY = self.velocityY - otherBall.velocityY;
							var relativeSpeed = relativeVelX * normalX + relativeVelY * normalY;
							if (relativeSpeed > 0) {
								// Calculate mass-based collision response (assume equal mass)
								var restitution = 0.3; // Lower restitution for more stable stacking
								var impulse = (1 + restitution) * relativeSpeed * 0.5;
								self.velocityX -= impulse * normalX;
								self.velocityY -= impulse * normalY;
								otherBall.velocityX += impulse * normalX;
								otherBall.velocityY += impulse * normalY;
								// Apply different friction based on collision angle
								var collisionAngle = Math.atan2(normalY, normalX);
								var frictionFactor = Math.abs(Math.sin(collisionAngle)) * 0.8 + 0.2;
								self.velocityX *= self.friction * frictionFactor;
								self.velocityY *= self.friction * frictionFactor;
								otherBall.velocityX *= otherBall.friction * frictionFactor;
								otherBall.velocityY *= otherBall.friction * frictionFactor;
								// Enhanced static detection for stacking
								if (Math.abs(self.velocityY) < 0.5 && Math.abs(self.velocityX) < 0.5) {
									// Check if ball is supported (has another ball below it or touching ground)
									var supportFound = false;
									var localBottom = gameAreaHeight - 20;
									// Check ground support
									if (self.y + self.radius >= localBottom - 5) {
										supportFound = true;
									} else {
										// Check support from other balls
										for (var j = 0; j < balls.length; j++) {
											var supportBall = balls[j];
											if (supportBall === self || supportBall.hasBeenMerged) continue;
											var supportDx = supportBall.x - self.x;
											var supportDy = supportBall.y - self.y;
											var supportDistance = Math.sqrt(supportDx * supportDx + supportDy * supportDy);
											// Check if this ball is resting on another ball (other ball is below)
											if (supportDistance < (self.radius + supportBall.radius) * 1.1 && supportDy > 0) {
												supportFound = true;
												break;
											}
										}
									}
									if (supportFound) {
										self.isStatic = true;
										self.velocityX = 0;
										self.velocityY = 0;
									}
								}
							}
						}
						collisionOccurred = true;
						break;
					}
				}
			}
			// If no collision occurred, move to intended position
			if (!collisionOccurred) {
				self.x = newX;
				self.y = newY;
			}
			// Boundary collisions after position update
			var localWidth = gameAreaRight - gameAreaLeft;
			var localBottom = gameAreaHeight - 20; // gameAreaHeight minus bottom wall height
			// Ground collision
			if (self.y + self.radius > localBottom) {
				self.y = localBottom - self.radius;
				self.velocityY *= -self.bounce;
				self.velocityX *= self.friction;
				if (Math.abs(self.velocityY) < 1) {
					self.velocityY = 0;
					self.isStatic = true;
				}
			}
			// Side walls collision
			if (self.x - self.radius < 0) {
				self.x = self.radius;
				self.velocityX *= -self.bounce;
			}
			if (self.x + self.radius > localWidth) {
				self.x = localWidth - self.radius;
				self.velocityX *= -self.bounce;
			}
		}
		// Enhanced merge detection pass - check for nearby same-numbered balls
		for (var i = 0; i < balls.length; i++) {
			var otherBall = balls[i];
			if (otherBall === self || otherBall.hasBeenMerged || otherBall.mergeTimer > 0 || self.mergeTimer > 0) continue;
			var dx = otherBall.x - self.x;
			var dy = otherBall.y - self.y;
			var distance = Math.sqrt(dx * dx + dy * dy);
			var minDistance = (self.radius + otherBall.radius) * 0.94; // Slightly looser packing
			// Enhanced merge sensitivity for same-numbered balls
			if (self.value === otherBall.value) {
				var mergeTriggerDistance = minDistance * 1.4; // Increased sensitivity to compensate for reduced minDistance
				if (distance < mergeTriggerDistance) {
					// Special case: when two 2048 balls merge, they explode and disappear
					if (self.value === 2048) {
						// Create explosion effect for both 2048 balls
						tween(self, {
							scaleX: 3.0,
							scaleY: 3.0,
							alpha: 0
						}, {
							duration: 600,
							easing: tween.easeOut
						});
						tween(otherBall, {
							scaleX: 3.0,
							scaleY: 3.0,
							alpha: 0
						}, {
							duration: 600,
							easing: tween.easeOut
						});
						// Mark both balls for removal
						self.hasBeenMerged = true;
						otherBall.hasBeenMerged = true;
						LK.getSound('merge').play();
						if (scoringActive) {
							LK.setScore(LK.getScore() + 1);
							scoreText.setText(LK.getScore());
						}
						return;
					}
					var newValue = self.value * 2;
					if (newValue <= 2048) {
						var newBall = new Ball(newValue);
						newBall.x = (self.x + otherBall.x) / 2;
						newBall.y = (self.y + otherBall.y) / 2;
						newBall.velocityX = (self.velocityX + otherBall.velocityX) / 2;
						newBall.velocityY = (self.velocityY + otherBall.velocityY) / 2;
						newBall.mergeTimer = 10;
						// Add merge animation - scale and pulse effect
						newBall.scaleX = 0.2; // Start very small
						newBall.scaleY = 0.2; // Start very small
						newBall.alpha = 0.7; // Start semi-transparent
						tween(newBall, {
							scaleX: 1.3,
							scaleY: 1.3,
							alpha: 1
						}, {
							duration: 200,
							easing: tween.easeOut,
							onFinish: function onFinish() {
								// Pulse back to normal size
								tween(newBall, {
									scaleX: 1,
									scaleY: 1
								}, {
									duration: 150,
									easing: tween.easeInOut
								});
							}
						});
						// Animate merging balls with shrink and fade
						tween(self, {
							scaleX: 0,
							scaleY: 0,
							alpha: 0
						}, {
							duration: 250,
							easing: tween.easeIn
						});
						tween(otherBall, {
							scaleX: 0,
							scaleY: 0,
							alpha: 0
						}, {
							duration: 250,
							easing: tween.easeIn
						});
						balls.push(newBall);
						gameArea.addChild(newBall);
						// Mark for removal
						self.hasBeenMerged = true;
						otherBall.hasBeenMerged = true;
						LK.getSound('merge').play();
						if (scoringActive) {
							LK.setScore(LK.getScore() + 1);
							scoreText.setText(LK.getScore());
						}
						// No win condition - game continues even after reaching 2048
						return;
					}
				}
			}
			// Enhanced separation for stable stacking
			if (distance < minDistance && distance > 0) {
				var overlap = minDistance - distance;
				var normalX = dx / distance;
				var normalY = dy / distance;
				// Different separation strategies based on ball positions
				var separationForce = overlap * 0.5;
				// If one ball is static and the other is moving, adjust separation
				if (self.isStatic && !otherBall.isStatic) {
					// Static ball moves less
					self.x -= normalX * separationForce * 0.2;
					self.y -= normalY * separationForce * 0.2;
					otherBall.x += normalX * separationForce * 0.8;
					otherBall.y += normalY * separationForce * 0.8;
				} else if (!self.isStatic && otherBall.isStatic) {
					// Other ball is static
					self.x -= normalX * separationForce * 0.8;
					self.y -= normalY * separationForce * 0.8;
					otherBall.x += normalX * separationForce * 0.2;
					otherBall.y += normalY * separationForce * 0.2;
				} else {
					// Both moving or both static - equal separation
					self.x -= normalX * separationForce;
					self.y -= normalY * separationForce;
					otherBall.x += normalX * separationForce;
					otherBall.y += normalY * separationForce;
				}
				// Ensure balls stay within bounds after separation
				var localWidth = gameAreaRight - gameAreaLeft;
				var localBottom = gameAreaHeight - 20; // gameAreaHeight minus bottom wall height
				self.x = Math.max(self.radius, Math.min(localWidth - self.radius, self.x));
				self.y = Math.max(self.radius, Math.min(localBottom - self.radius, self.y));
				// Re-check static state after separation
				if (self.isStatic && (Math.abs(self.x - (self.x - normalX * separationForce)) > 1 || Math.abs(self.y - (self.y - normalY * separationForce)) > 1)) {
					self.isStatic = false; // Ball was moved significantly, make it dynamic again
				}
			}
		}
		// Periodic re-evaluation of static state for better stacking
		if (LK.ticks % 10 === 0 && !self.hasBeenMerged) {
			// Check every 10 frames
			if (!self.isStatic && Math.abs(self.velocityX) < 0.3 && Math.abs(self.velocityY) < 0.3) {
				// Check if ball should become static
				var supportFound = false;
				var localBottom = gameAreaHeight - 20;
				// Check ground support
				if (self.y + self.radius >= localBottom - 2) {
					supportFound = true;
				} else {
					// Check support from other static balls
					for (var k = 0; k < balls.length; k++) {
						var checkBall = balls[k];
						if (checkBall === self || checkBall.hasBeenMerged || !checkBall.isStatic) continue;
						var checkDx = checkBall.x - self.x;
						var checkDy = checkBall.y - self.y;
						var checkDistance = Math.sqrt(checkDx * checkDx + checkDy * checkDy);
						// Ball is resting on another static ball
						if (checkDistance < (self.radius + checkBall.radius) * 1.05 && checkDy > 0) {
							supportFound = true;
							break;
						}
					}
				}
				if (supportFound) {
					self.isStatic = true;
					self.velocityX = 0;
					self.velocityY = 0;
				}
			} else if (self.isStatic) {
				// Check if static ball should become dynamic (lost support)
				var stillSupported = false;
				var localBottom = gameAreaHeight - 20;
				// Check ground support
				if (self.y + self.radius >= localBottom - 2) {
					stillSupported = true;
				} else {
					// Check support from other balls
					for (var k = 0; k < balls.length; k++) {
						var supportBall = balls[k];
						if (supportBall === self || supportBall.hasBeenMerged) continue;
						var supportDx = supportBall.x - self.x;
						var supportDy = supportBall.y - self.y;
						var supportDistance = Math.sqrt(supportDx * supportDx + supportDy * supportDy);
						if (supportDistance < (self.radius + supportBall.radius) * 1.05 && supportDy > 0) {
							stillSupported = true;
							break;
						}
					}
				}
				if (!stillSupported) {
					self.isStatic = false; // Ball lost support, make it fall
				}
			}
		}
	};
	return self;
});
/**** 
* Initialize Game
****/ 
var game = new LK.Game({
	backgroundColor: 0xf5f5dc
});
/**** 
* Game Code
****/ 
var balls = [];
var nextBallValue = 2;
var gameAreaLeft = 0;
var gameAreaRight = 2048;
var gameAreaTop = 0;
var gameAreaBottom = 2732;
var dropCooldown = 0;
var gameAreaHeight = gameAreaBottom - gameAreaTop;
var dangerZoneImmunity = 300; // 5 seconds at 60fps
var scoringActive = false; // Flag to track when scoring should be active
// Create game area background
var gameAreaBg = game.addChild(LK.getAsset('gameArea', {
	anchorX: 0.5,
	anchorY: 0,
	x: 1024,
	y: gameAreaTop
}));
// Create game area container for balls
var gameArea = new Container();
gameArea.x = gameAreaLeft;
gameArea.y = gameAreaTop;
game.addChild(gameArea);
// Create visible walls
var leftWall = game.addChild(LK.getAsset('leftWall', {
	anchorX: 1,
	anchorY: 0,
	x: gameAreaLeft,
	y: gameAreaTop
}));
var rightWall = game.addChild(LK.getAsset('rightWall', {
	anchorX: 0,
	anchorY: 0,
	x: gameAreaRight,
	y: gameAreaTop
}));
var bottomWall = game.addChild(LK.getAsset('bottomWall', {
	anchorX: 0.5,
	anchorY: 0,
	x: 1024,
	y: gameAreaBottom
}));
// Create danger zone line (visible red line showing danger threshold)
var dangerZoneLine = game.addChild(LK.getAsset('gameOverLine', {
	anchorX: 0.5,
	anchorY: 0.5,
	x: 1024,
	y: gameAreaTop + 350,
	alpha: 0.8,
	visible: true
}));
// Create score display
var scoreText = new Text2('0', {
	size: 100,
	fill: 0xFFFFFF
});
scoreText.anchor.set(1, 0);
LK.gui.topRight.addChild(scoreText);
scoreText.x = -50; // Small offset from right edge
scoreText.y = 100;
// Create next ball preview
var nextBallPreview = LK.getAsset('ball' + nextBallValue, {
	anchorX: 0.5,
	anchorY: 1.0,
	x: 1024,
	y: gameAreaTop + 350,
	scaleX: 1.4,
	scaleY: 1.4
});
game.addChild(nextBallPreview);
// Create next ball value text
var nextBallText = new Text2(nextBallValue.toString(), {
	size: 48,
	fill: 0xFFFFFF
});
nextBallText.anchor.set(0.5, 0.5);
nextBallText.x = 1024;
nextBallText.y = gameAreaTop + 200;
game.addChild(nextBallText);
function getRandomNextBallValue() {
	var values = [2, 4, 8];
	return values[Math.floor(Math.random() * values.length)];
}
function updateNextBallPreview() {
	game.removeChild(nextBallPreview);
	game.removeChild(nextBallText);
	nextBallPreview = LK.getAsset('ball' + nextBallValue, {
		anchorX: 0.5,
		anchorY: 1.0,
		x: 1024,
		y: gameAreaTop + 350,
		scaleX: 1.4,
		scaleY: 1.4
	});
	game.addChild(nextBallPreview);
	nextBallText = new Text2(nextBallValue.toString(), {
		size: 48,
		fill: 0xFFFFFF
	});
	nextBallText.anchor.set(0.5, 0.5);
	nextBallText.x = 1024;
	nextBallText.y = gameAreaTop + 200;
	game.addChild(nextBallText);
}
function checkGameOver() {
	// Skip game over check during immunity period
	if (dangerZoneImmunity > 0) return;
	for (var i = 0; i < balls.length; i++) {
		// Check if ball touches the danger zone (first 350 pixels of game area)
		// Since balls are in gameArea container, we use local coordinates
		// Only trigger game over if ball is static (not actively falling) and in danger zone
		if (balls[i].y - balls[i].radius <= 350 && balls[i].isStatic) {
			LK.showGameOver();
			return;
		}
	}
}
function cleanupMergedBalls() {
	for (var i = balls.length - 1; i >= 0; i--) {
		if (balls[i].hasBeenMerged) {
			gameArea.removeChild(balls[i]);
			balls[i].destroy();
			balls.splice(i, 1);
		}
	}
}
// Create initial balls to fill container to the top
function createInitialBalls() {
	var localWidth = gameAreaRight - gameAreaLeft;
	var ballsPerRow = 7; // Fixed number of balls per row for consistent layout
	var rowBalls = []; // Track balls in current row for spacing calculation
	var rowHeights = []; // Track row heights for stacking
	var previousRowBalls = []; // Track balls from previous row for vertical spacing
	for (var row = 0; row < 12; row++) {
		rowBalls = []; // Reset for each row
		var maxRadiusInRow = 0; // Track largest ball in current row
		// Create balls for this row first to know their sizes
		var ballsData = [];
		for (var col = 0; col < ballsPerRow; col++) {
			var ballValue = getRandomNextBallValue();
			var radiusMap = {
				2: 90,
				4: 95,
				8: 105,
				16: 115,
				32: 125,
				64: 135,
				128: 145,
				256: 155,
				512: 165,
				1024: 175,
				2048: 185
			};
			var ballRadius = radiusMap[ballValue] || 60;
			ballsData.push({
				value: ballValue,
				radius: ballRadius
			});
			maxRadiusInRow = Math.max(maxRadiusInRow, ballRadius);
		}
		// Calculate X positions to make balls touch tightly horizontally - subtract overlap for tighter packing
		var xPositions = [];
		var currentX = 0;
		var overlapAmount = 8; // Slightly reduced overlap for looser horizontal packing
		for (var col = 0; col < ballsPerRow; col++) {
			if (col === 0) {
				// First ball starts at its radius
				currentX = ballsData[col].radius;
			} else {
				// Each subsequent ball is positioned closer than touching distance
				var prevRadius = ballsData[col - 1].radius;
				var currentRadius = ballsData[col].radius;
				currentX += prevRadius + currentRadius - overlapAmount; // Reduce spacing for tighter packing
			}
			xPositions.push(currentX);
		}
		// Center the entire row
		var totalRowWidth = xPositions[xPositions.length - 1] + ballsData[ballsPerRow - 1].radius;
		var offsetX = (localWidth - totalRowWidth) / 2;
		// Create and position the balls
		for (var col = 0; col < ballsPerRow; col++) {
			var initialBall = new Ball(ballsData[col].value);
			initialBall.x = xPositions[col] + offsetX;
			initialBall.velocityX = 0;
			initialBall.velocityY = 0;
			initialBall.isStatic = true; // Start as static for stable stacking
			rowBalls.push(initialBall);
			balls.push(initialBall);
			gameArea.addChild(initialBall);
		}
		// Calculate Y position to make balls touch tightly vertically with previous row
		var rowY;
		var verticalOverlap = 4; // Slightly reduced vertical overlap for looser packing
		if (row === 0) {
			// First row sits on bottom
			rowY = gameAreaHeight - 100 - maxRadiusInRow;
		} else {
			// For subsequent rows, find the minimum Y position with tighter packing
			rowY = gameAreaHeight; // Start with max possible Y
			for (var currentCol = 0; currentCol < rowBalls.length; currentCol++) {
				var currentBall = rowBalls[currentCol];
				var currentBallRadius = currentBall.radius;
				var minYForThisBall = gameAreaHeight - 100 - currentBallRadius; // Default to bottom
				// Check against all balls in previous row
				for (var prevCol = 0; prevCol < previousRowBalls.length; prevCol++) {
					var prevBall = previousRowBalls[prevCol];
					var dx = currentBall.x - prevBall.x;
					var dy = 0; // We'll calculate the required dy
					var horizontalDistance = Math.abs(dx);
					var requiredCenterDistance = currentBallRadius + prevBall.radius - verticalOverlap; // Tighter vertical packing
					// If balls are close enough horizontally to potentially touch
					if (horizontalDistance < requiredCenterDistance) {
						// Calculate the vertical distance needed for tighter packing
						var verticalDistance = Math.sqrt(Math.max(0, requiredCenterDistance * requiredCenterDistance - dx * dx));
						var requiredY = prevBall.y - verticalDistance;
						minYForThisBall = Math.min(minYForThisBall, requiredY);
					}
				}
				// The row Y is determined by the ball that needs to be highest (smallest Y)
				rowY = Math.min(rowY, minYForThisBall);
			}
		}
		// Apply the calculated Y position to all balls in this row
		for (var j = 0; j < rowBalls.length; j++) {
			rowBalls[j].y = rowY;
		}
		// Store this row's balls for next row calculation
		previousRowBalls = rowBalls.slice(); // Copy the array
	}
}
// Initialize the starting balls
createInitialBalls();
game.down = function (x, y, obj) {
	if (dropCooldown > 0) return;
	// Activate scoring system when first ball is dropped
	if (!scoringActive) {
		scoringActive = true;
	}
	// Constrain drop position to game area (convert to local coordinates)
	var localWidth = gameAreaRight - gameAreaLeft;
	var localX = x - gameAreaLeft;
	var dropX = Math.max(80, Math.min(localWidth - 80, localX));
	var newBall = new Ball(nextBallValue);
	newBall.x = dropX;
	newBall.y = 420; // Start below the danger zone (350px) plus ball radius (60px) plus buffer
	newBall.velocityX = 0;
	newBall.velocityY = 0;
	balls.push(newBall);
	gameArea.addChild(newBall);
	LK.getSound('drop').play();
	// Update next ball
	nextBallValue = getRandomNextBallValue();
	updateNextBallPreview();
	dropCooldown = 30; // 0.5 seconds at 60fps
};
game.update = function () {
	if (dropCooldown > 0) {
		dropCooldown--;
	}
	// Handle danger zone immunity countdown
	if (dangerZoneImmunity > 0) {
		dangerZoneImmunity--;
	}
	cleanupMergedBalls();
	checkGameOver();
	// Update score display
	scoreText.setText(LK.getScore());
};
 Koyu yeşil bilye. In-Game asset. 2d. High contrast. No shadows
 Kahverengi bilye. In-Game asset. 2d. High contrast. No shadows
 Bprdo renk bilye. In-Game asset. 2d. High contrast. No shadows
 Açık kahve bilye. In-Game asset. 2d. High contrast. No shadows
 Gri bilye. In-Game asset. 2d. High contrast. No shadows