User prompt
Ava, can you please fix the score text box, as it is not present on the screen? I cannot see it
User prompt
the score text is no longer visible, please add it back. make sure to make it black and palce it in the GUI layer, under the game over bar
User prompt
the score text is no longer visible, please add it back
User prompt
change the background color to 0xffe122
User prompt
the trajectory line adds dots at increasingly longer distances, the distance between each dot should always be the same
User prompt
The trajectory line must dynamically and accurately shorten its visible path to terminate precisely where it first predicts an intersection with: The game floor. Any existing, non-static fruit in the play area. This involves ensuring the collision detection logic used for trajectory prediction is robust and that the line's rendering stops at the calculated intersection point.
User prompt
there's something that still makes fruits keep spinning when they shouldn't. I can't exacly tell what, if it's either a forced force that imprints rotation or movement, but fruits should ultimately reach a standstill. they should mimic real life physics, where everything must imprint some sort of force that stops fruits since this force adds friction. this only happens to fruits that are above other fruits, and are not touching walls or the floor. the ones that do touch walls or the floor seem to come to a rest sooner
Code edit (1 edits merged)
Please save this source code
User prompt
the TRAJECTORY_LINE_Y_OFFSET should be relative to the fruit's bottom, not it's center
Code edit (1 edits merged)
Please save this source code
User prompt
make the 50 pixels under the current fruit starting point you just added, into a global variable which is called from all places, so you dont hardcode it
User prompt
hmmm, this is better but now it looks a bit too bad, actually make the trajectory line start 50 pixels from under the current fruit
User prompt
nice, you fixed it my glip glop! now, can we also fix the trajectory line, not starting from the actual y position of the active fruit? right now it starts a bit lower, please fix that Ava
User prompt
there's something that still makes fruits keep spinning when they shouldn't. I can't exacly tell what, if it's either a forced force that imprints rotation or movement, but fruits should ultimately reach a standstill. they should mimic real life physics, where everything must imprint some sort of force that stops fruits since this force adds friction.
User prompt
there's something that still makes fruits keep spinning when they shouldn't. I can't exacly tell what, if it's either a forced force that imprints rotation or movement, but fruits should ultimately reach a standstill. they should mimic real life physics, where everything must imprint some sort of force that stops fruits since this force adds friction.
User prompt
add a 200 miliseconds delay before loading the next fruit
User prompt
the trajectory line should not intersect with fruits that are still falling. fruits that have not yet touches the floor or another fruit, should not affect the trajectory line
User prompt
the trajectory line follows fruits contour, but it looks like it folows a rectangle shape instead of a circular one. fruits are either circular or elipticl butdefinitely not rectangular. so fix that please
Code edit (1 edits merged)
Please save this source code
User prompt
the active fruit is not being dropped from the same place. when I tap to drop a fruit, it's being released from much lower then the active fruit, as if a new one is created. the fruit that is being dragged is the same that should drop on releasing the finger
User prompt
The trajectory rendering mechanism must be capable of drawing a line that can visually span the entire vertical distance from the designated fruit drop point down to the game's floor level, assuming no intervening fruit obstacles. This requires adjusting the internal limit on the number of segments or dots the trajectory can display.
User prompt
In the TrajectoryLine class, the maxDots variable (currently 50) limits how many dots can be drawn. You need to calculate a more appropriate value for maxDots. The line should be able to extend from the fruit's drop point (dropPointY + GAME_CONSTANTS.DROP_START_Y_OFFSET) all the way to the game floor (gameFloor.y). Calculation: dropStartY = GAME_CONSTANTS.DROP_POINT_Y + GAME_CONSTANTS.DROP_START_Y_OFFSET distanceToFloor = GAME_CONSTANTS.GAME_HEIGHT - dropStartY (assuming gameFloor.y is at GAME_HEIGHT) requiredDots = Math.ceil(distanceToFloor / self.dotSpacing) Set self.maxDots to this requiredDots value, or a slightly larger fixed number that generously covers this distance (e.g., 100 or 120, then test). This will allow the line to physically reach the bottom if no obstacles are encountered. Example (inside TrajectoryLine): // Inside TrajectoryLine class // self.maxDots = 50; // Old value // Calculate based on game dimensions and spacing for better coverage var dropActualY = GAME_CONSTANTS.DROP_POINT_Y + GAME_CONSTANTS.DROP_START_Y_OFFSET; var distanceToCover = GAME_CONSTANTS.GAME_HEIGHT - dropActualY; self.maxDots = Math.ceil(distanceToCover / self.dotSpacing) + 5; // +5 for a little buffer if (self.maxDots <= 0) self.maxDots = 60; // Fallback if calculation is off
User prompt
the trajectory line is still bugged. it doesnt extend as it should, from the current fruit location all the way to the bottom, or the first fruit it touches, instead it's condensed and not dynamic. make the first dot appear from right under th first fruit, and ensure it extends all the way to the first fruit it encounters, as right now it stops way too son
User prompt
the trajectory line is bugged. first off, it doesnt extend as it should, from the current fruit location all the way to the bottom, or the first fruit it touches, instead it's condensed and not dynamic. and also, after releasing a fruit, the trajectory line breaks and turns into a single dot, and onlt starts working again after I start moving by figner on the screen again, but th trajectory line should be the same regardless of where the figner. when a new fruit is created, ensure the trajectory line works by default without having to be activated by the finger
Code edit (2 edits merged)
Please save this source code
/**** 
* Plugins
****/ 
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/**** 
* Classes
****/ 
var ChargedBallUI = Container.expand(function () {
	var self = Container.call(this);
	self.chargeNeededForRelease = GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE;
	self.currentCharge = 0;
	self.isReadyToRelease = false;
	self.pulseAnimationActive = false;
	self.initialize = function () {
		self.portalAsset = self.attachAsset('portal', {
			anchorX: 0.5,
			anchorY: 0.5
		});
		self.portalAsset.x = GAME_CONSTANTS.GAME_WIDTH / 2 + GAME_CONSTANTS.PORTAL_UI_X_OFFSET;
		self.portalAsset.alpha = 0;
		self.portalAsset.scaleX = 0;
		self.portalAsset.scaleY = 0;
		self.y = GAME_CONSTANTS.PORTAL_UI_Y;
	};
	self.updateChargeDisplay = function (chargeCount) {
		self.currentCharge = chargeCount;
		var remainingCount = Math.max(0, self.chargeNeededForRelease - self.currentCharge);
		var progressPercent = (self.chargeNeededForRelease - remainingCount) / self.chargeNeededForRelease;
		var targetScale = progressPercent;
		if (progressPercent > 0 && self.portalAsset.alpha === 0) {
			self.portalAsset.alpha = 1;
		}
		tween(self.portalAsset, {
			scaleX: targetScale,
			scaleY: targetScale,
			alpha: progressPercent
		}, {
			duration: GAME_CONSTANTS.PORTAL_TWEEN_DURATION,
			easing: tween.easeOut
		});
		if (remainingCount === 0 && !self.isReadyToRelease) {
			self.setReadyState(true);
		}
	};
	self.setReadyState = function (isReady) {
		self.isReadyToRelease = isReady;
		if (isReady) {
			tween(self.portalAsset, {
				scaleX: 1.0,
				scaleY: 1.0,
				alpha: 1.0,
				rotation: Math.PI * 2
			}, {
				duration: GAME_CONSTANTS.PORTAL_TWEEN_DURATION,
				easing: tween.easeOut
			});
			self.startPulseAnimation();
		}
	};
	self.startPulseAnimation = function () {
		if (self.pulseAnimationActive) {
			return;
		}
		self.pulseAnimationActive = true;
		self._pulseText();
	};
	self._pulseText = function () {
		if (!self.isReadyToRelease) {
			self.pulseAnimationActive = false;
			return;
		}
		tween(self.portalAsset, {
			scaleX: 1.3,
			scaleY: 1.3
		}, {
			duration: GAME_CONSTANTS.PORTAL_PULSE_DURATION,
			easing: tween.easeInOut,
			onFinish: function onFinish() {
				if (!self.isReadyToRelease) {
					self.pulseAnimationActive = false;
					return;
				}
				tween(self.portalAsset, {
					scaleX: 1.0,
					scaleY: 1.0
				}, {
					duration: GAME_CONSTANTS.PORTAL_PULSE_DURATION,
					easing: tween.easeInOut,
					onFinish: self._pulseText
				});
			}
		});
	};
	self.reset = function () {
		self.isReadyToRelease = false;
		self.currentCharge = 0;
		self.pulseAnimationActive = false;
		tween(self.portalAsset, {
			alpha: 0
		}, {
			duration: 200,
			easing: tween.easeOut
		});
		tween(self.portalAsset, {
			scaleX: 0,
			scaleY: 0
		}, {
			duration: 200,
			easing: tween.easeOut
		});
	};
	self.initialize();
	return self;
});
var CollisionComponent = Container.expand(function () {
	var self = Container.call(this);
	// wallContactFrames is now primarily for tracking if there *is* contact,
	// rather than complex progressive friction which has been simplified.
	self.wallContactFrames = 0;
	self.checkBoundaryCollisions = function (fruit, walls, floor) {
		if (!walls || !walls.left || !walls.right || !floor) {
			return;
		}
		var fruitHalfWidth = fruit.width / 2;
		var fruitHalfHeight = fruit.height / 2;
		// Simplified effective dimension calculation - can be refined if necessary
		var cosAngle = Math.abs(Math.cos(fruit.rotation));
		var sinAngle = Math.abs(Math.sin(fruit.rotation));
		var effectiveWidth = fruitHalfWidth * cosAngle + fruitHalfHeight * sinAngle;
		var effectiveHeight = fruitHalfHeight * cosAngle + fruitHalfWidth * sinAngle;
		fruit._boundaryContacts = fruit._boundaryContacts || {
			left: false,
			right: false,
			floor: false
		};
		// Reset contacts each frame before checking
		fruit._boundaryContacts.left = false;
		fruit._boundaryContacts.right = false;
		fruit._boundaryContacts.floor = false;
		self.checkLeftWallCollision(fruit, walls.left, effectiveWidth);
		self.checkRightWallCollision(fruit, walls.right, effectiveWidth);
		self.checkFloorCollision(fruit, floor, effectiveHeight);
		var isInContactWithBoundary = fruit._boundaryContacts.left || fruit._boundaryContacts.right || fruit._boundaryContacts.floor;
		if (isInContactWithBoundary) {
			self.wallContactFrames = Math.min(self.wallContactFrames + 1, GAME_CONSTANTS.MAX_WALL_CONTACT_FRAMES_FOR_FRICTION_EFFECT); // Cap frames for effect
			// Apply a consistent friction/damping if in contact with wall/floor
			// This is now handled more directly in PhysicsComponent based on contact flags
		} else {
			self.wallContactFrames = Math.max(0, self.wallContactFrames - 1);
		}
	};
	self.checkLeftWallCollision = function (fruit, leftWall, effectiveWidth) {
		var leftBoundary = leftWall.x + leftWall.width / 2 + effectiveWidth;
		if (fruit.x < leftBoundary) {
			var incomingVx = fruit.vx;
			fruit.x = leftBoundary;
			// Simplified bounce, elasticity is now more central. WALL_BOUNCE_DAMPING ensures some energy loss.
			fruit.vx = -incomingVx * fruit.elasticity * GAME_CONSTANTS.WALL_BOUNCE_DAMPING;
			if (Math.abs(incomingVx) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) {
				LK.getSound('bounce').play();
			}
			// Simplified angular effect from wall collision
			fruit.angularVelocity += fruit.vy * GAME_CONSTANTS.WALL_ANGULAR_IMPULSE_FACTOR;
			fruit.angularVelocity *= GAME_CONSTANTS.WALL_ANGULAR_DAMPING;
			fruit._boundaryContacts.left = true;
		}
	};
	self.checkRightWallCollision = function (fruit, rightWall, effectiveWidth) {
		var rightBoundary = rightWall.x - rightWall.width / 2 - effectiveWidth;
		if (fruit.x > rightBoundary) {
			var incomingVx = fruit.vx;
			fruit.x = rightBoundary;
			fruit.vx = -incomingVx * fruit.elasticity * GAME_CONSTANTS.WALL_BOUNCE_DAMPING;
			if (Math.abs(incomingVx) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) {
				LK.getSound('bounce').play();
			}
			fruit.angularVelocity -= fruit.vy * GAME_CONSTANTS.WALL_ANGULAR_IMPULSE_FACTOR;
			fruit.angularVelocity *= GAME_CONSTANTS.WALL_ANGULAR_DAMPING;
			fruit._boundaryContacts.right = true;
		}
	};
	self.checkFloorCollision = function (fruit, floor, effectiveHeight) {
		var floorCollisionY = floor.y - floor.height / 2 - effectiveHeight;
		if (fruit.y > floorCollisionY) {
			var incomingVy = fruit.vy;
			fruit.y = floorCollisionY;
			fruit.vy = -incomingVy * fruit.elasticity * GAME_CONSTANTS.FLOOR_BOUNCE_DAMPING;
			if (Math.abs(incomingVy) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) {
				LK.getSound('bounce').play();
			}
			fruit._boundaryContacts.floor = true;
			// Apply friction to linear velocity when on floor (more direct than before)
			fruit.vx *= GAME_CONSTANTS.GROUND_LINEAR_FRICTION;
			// Simpler angular velocity effect from floor contact (rolling)
			if (Math.abs(fruit.vx) > GAME_CONSTANTS.MIN_VX_FOR_ROLLING) {
				// A small portion of vx is converted to angularVelocity to simulate rolling
				var targetAngular = fruit.vx * GAME_CONSTANTS.GROUND_ROLLING_FACTOR;
				// Blend towards target rolling angular velocity
				fruit.angularVelocity = fruit.angularVelocity * 0.7 + targetAngular * 0.3;
			}
			fruit.angularVelocity *= GAME_CONSTANTS.GROUND_ANGULAR_DAMPING; // General angular damping on ground
			// Simplified resting condition
			if (Math.abs(fruit.vy) < GAME_CONSTANTS.RESTING_VELOCITY_THRESHOLD) {
				fruit.vy = 0;
			}
			if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.ANGULAR_RESTING_THRESHOLD) {
				fruit.angularVelocity = 0;
			}
		}
	};
	return self;
});
var DotPool = Container.expand(function (initialSize) {
	var self = Container.call(this);
	var pool = [];
	var activeObjects = [];
	self.initialize = function (size) {
		for (var i = 0; i < size; i++) {
			self.createObject();
		}
	};
	self.createObject = function () {
		var dot = new Container();
		var dotGraphic = dot.attachAsset('trajectoryDot', {
			anchorX: 0.5,
			anchorY: 0.5
		});
		dotGraphic.tint = 0xFFFFFF;
		dot.scaleX = 0.8;
		dot.scaleY = 0.8;
		dot.visible = false;
		pool.push(dot);
		return dot;
	};
	self.get = function () {
		var object = pool.length > 0 ? pool.pop() : self.createObject();
		activeObjects.push(object);
		return object;
	};
	self.release = function (object) {
		var index = activeObjects.indexOf(object);
		if (index !== -1) {
			activeObjects.splice(index, 1);
			object.visible = false;
			pool.push(object);
		}
	};
	self.releaseAll = function () {
		while (activeObjects.length > 0) {
			var object = activeObjects.pop();
			object.visible = false;
			pool.push(object);
		}
	};
	if (initialSize) {
		self.initialize(initialSize);
	}
	return self;
});
var EvolutionLine = Container.expand(function () {
	var self = Container.call(this);
	self.initialize = function () {
		self.y = GAME_CONSTANTS.EVOLUTION_LINE_Y;
		self.x = GAME_CONSTANTS.GAME_WIDTH / 2;
		var fruitTypes = [FruitTypes.CHERRY, FruitTypes.GRAPE, FruitTypes.APPLE, FruitTypes.ORANGE, FruitTypes.WATERMELON, FruitTypes.PINEAPPLE, FruitTypes.MELON, FruitTypes.PEACH, FruitTypes.COCONUT, FruitTypes.DURIAN];
		var totalWidth = 0;
		var fruitIcons = [];
		for (var i = 0; i < fruitTypes.length; i++) {
			var fruitType = fruitTypes[i];
			var fruitIcon = LK.getAsset(fruitType.id, {
				anchorX: 0.5,
				anchorY: 0.5
			});
			var scale = Math.min(GAME_CONSTANTS.EVOLUTION_ICON_MAX_SIZE / fruitIcon.width, GAME_CONSTANTS.EVOLUTION_ICON_MAX_SIZE / fruitIcon.height);
			fruitIcon.scaleX = scale;
			fruitIcon.scaleY = scale;
			totalWidth += fruitIcon.width * scale;
			if (i < fruitTypes.length - 1) {
				totalWidth += GAME_CONSTANTS.EVOLUTION_ICON_SPACING;
			}
			fruitIcons.push(fruitIcon);
		}
		var currentX = -totalWidth / 2;
		for (var i = 0; i < fruitIcons.length; i++) {
			var icon = fruitIcons[i];
			icon.x = currentX + icon.width * icon.scaleX / 2;
			icon.y = 0;
			self.addChild(icon);
			currentX += icon.width * icon.scaleX + GAME_CONSTANTS.EVOLUTION_ICON_SPACING;
		}
	};
	// Call initialize in the constructor pattern
	self.initialize();
	return self;
});
var FireElement = Container.expand(function (initX, initY, zIndex) {
	var self = Container.call(this);
	self.baseX = initX || 0;
	self.baseY = initY || 0;
	self.zIndex = zIndex || 0;
	self.movementRange = 30 + Math.random() * 20;
	self.movementSpeed = 0.3 + Math.random() * 0.4;
	self.direction = Math.random() > 0.5 ? 1 : -1;
	self.alphaMin = GAME_CONSTANTS.FIRE_ALPHA_MIN;
	self.alphaMax = GAME_CONSTANTS.FIRE_ALPHA_MAX;
	self.flickerSpeed = GAME_CONSTANTS.FIRE_FLICKER_SPEED_BASE + Math.random() * GAME_CONSTANTS.FIRE_FLICKER_SPEED_RANDOM;
	self.frameIndex = 0;
	self.frameTimer = null;
	self.frameDuration = GAME_CONSTANTS.FIRE_FRAME_DURATION;
	self.initialize = function () {
		self.fireAsset = self.attachAsset('fire', {
			anchorX: 0.5,
			anchorY: 1.0
		});
		self.fireAsset2 = self.attachAsset('fire_2', {
			anchorX: 0.5,
			anchorY: 1.0
		});
		self.fireAsset2.visible = false;
		self.x = self.baseX;
		self.y = self.baseY;
		self.startAlphaFlicker();
		self.startFrameAnimation();
	};
	self.update = function () {
		self.x += self.movementSpeed * self.direction;
		if (Math.abs(self.x - self.baseX) > self.movementRange) {
			self.direction *= -1;
		}
	};
	self.startFrameAnimation = function () {
		if (self.frameTimer) {
			LK.clearInterval(self.frameTimer);
		}
		self.frameTimer = LK.setInterval(function () {
			self.toggleFrame();
		}, self.frameDuration);
	};
	self.toggleFrame = function () {
		self.frameIndex = (self.frameIndex + 1) % 2;
		self.fireAsset.visible = self.frameIndex === 0;
		self.fireAsset2.visible = self.frameIndex === 1;
	};
	self.startAlphaFlicker = function () {
		if (self.flickerTween) {
			self.flickerTween.stop();
		}
		var startDelay = Math.random() * 500;
		LK.setTimeout(function () {
			self.flickerToMax();
		}, startDelay);
	};
	self.flickerToMax = function () {
		self.flickerTween = tween(self, {
			alpha: self.alphaMax
		}, {
			duration: self.flickerSpeed,
			easing: tween.easeInOut,
			onFinish: self.flickerToMin
		});
	};
	self.flickerToMin = function () {
		self.flickerTween = tween(self, {
			alpha: self.alphaMin
		}, {
			duration: self.flickerSpeed,
			easing: tween.easeInOut,
			onFinish: self.flickerToMax
		});
	};
	self.destroy = function () {
		if (self.flickerTween) {
			self.flickerTween.stop();
		}
		if (self.frameTimer) {
			LK.clearInterval(self.frameTimer);
			self.frameTimer = null;
		}
		Container.prototype.destroy.call(this); // Call parent's destroy
	};
	self.initialize();
	return self;
});
var Fruit = Container.expand(function (type) {
	var self = Container.call(this);
	self.id = 'fruit_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
	self.type = type;
	// Components are instantiated here
	var physics = new PhysicsComponent(); // Manages physics updates, stabilization, sleeping
	var collision = new CollisionComponent(); // Manages boundary collisions
	var mergeHandler = new MergeComponent(); // Manages merging logic
	var behaviorSystem = new FruitBehavior(); // Manages type-specific behaviors
	// Physics properties are on the fruit itself, managed by PhysicsComponent
	self.vx = 0;
	self.vy = 0;
	self.rotation = 0;
	self.angularVelocity = 0;
	var currentLevel = getFruitLevel(self); // Helper to get fruit level
	self.gravity = GAME_CONSTANTS.BASE_GRAVITY * (1 + Math.pow(currentLevel, GAME_CONSTANTS.GRAVITY_LEVEL_POWER_SCALE) * GAME_CONSTANTS.GRAVITY_LEVEL_MULTIPLIER_ADJUSTED);
	// Elasticity: Higher level fruits are slightly less bouncy
	self.elasticity = Math.max(GAME_CONSTANTS.MIN_ELASTICITY, GAME_CONSTANTS.BASE_ELASTICITY - currentLevel * GAME_CONSTANTS.ELASTICITY_DECREASE_PER_LEVEL);
	// State flags
	self.isStatic = false; // If true, physics is not applied (e.g., next fruit display)
	self.isSleeping = false; // If true, physics updates are minimal (already resting)
	self.isFullyStabilized = false; // Intermediate state for coming to rest
	self._sleepCounter = 0; // Counter for consecutive frames with low energy to trigger sleeping
	self.wallContactFrames = 0; // Now simpler, tracked by CollisionComponent but can be read by Fruit if needed
	// Merge related properties
	self.merging = false; // True if this fruit is currently in a merge animation
	self.mergeGracePeriodActive = false; // Short period after drop where merges are prioritized
	self.fromChargedRelease = false; // If this fruit spawned from the special charge release
	// Game logic flags
	self.safetyPeriod = false; // Used in game over logic (original logic retained)
	self.immuneToGameOver = false; // If true, this fruit cannot trigger game over
	// Behavior (e.g., special merge effects)
	self.behavior = type && type.id ? behaviorSystem.getMergeHandler(type.id) : null;
	if (self.type && self.type.id && self.type.points && self.type.size) {
		var fruitGraphics = self.attachAsset(self.type.id, {
			anchorX: 0.5,
			anchorY: 0.5
		});
		self.width = fruitGraphics.width;
		self.height = fruitGraphics.height;
		if (self.behavior && self.behavior.onSpawn) {
			self.behavior.onSpawn(self);
		}
	} else {
		console.warn("Fruit: Type not available or missing required properties for fruit ID: " + (type ? type.id : 'unknown'));
	}
	// Public methods to interact with components
	self.updatePhysics = function () {
		// Delegate to the physics component, passing self (the fruit instance)
		physics.apply(self);
	};
	self.checkBoundaries = function (walls, floor) {
		// Delegate to the collision component
		collision.checkBoundaryCollisions(self, walls, floor);
		self.wallContactFrames = collision.wallContactFrames; // Update fruit's copy if needed elsewhere
	};
	self.merge = function (otherFruit) {
		// Delegate to the merge handler
		mergeHandler.beginMerge(self, otherFruit);
	};
	// Method to wake up a fruit if it's sleeping or stabilized
	self.wakeUp = function () {
		if (self.isSleeping || self.isFullyStabilized) {
			self.isSleeping = false;
			self.isFullyStabilized = false;
			self._sleepCounter = 0;
			// Optional: apply a tiny impulse if needed to ensure it starts moving
			// self.vy -= 0.1; 
		}
	};
	return self;
});
var FruitBehavior = Container.expand(function () {
	var self = Container.call(this);
	self.behaviors = {
		CHERRY: {
			onMerge: function onMerge(f1, f2, x, y) {
				return self.standardMerge(f1, f2, x, y);
			}
		},
		GRAPE: {
			onMerge: function onMerge(f1, f2, x, y) {
				return self.standardMerge(f1, f2, x, y);
			}
		},
		APPLE: {
			onMerge: function onMerge(f1, f2, x, y) {
				return self.standardMerge(f1, f2, x, y);
			}
		},
		ORANGE: {
			onMerge: function onMerge(f1, f2, x, y) {
				return self.standardMerge(f1, f2, x, y);
			}
		},
		WATERMELON: {
			onMerge: function onMerge(f1, f2, x, y) {
				return self.standardMerge(f1, f2, x, y);
			}
		},
		PINEAPPLE: {
			onMerge: function onMerge(f1, f2, x, y) {
				return self.standardMerge(f1, f2, x, y);
			},
			onSpawn: function onSpawn() {}
		},
		MELON: {
			onMerge: function onMerge(f1, f2, x, y) {
				LK.getSound('Smartz').play();
				return self.standardMerge(f1, f2, x, y);
			}
		},
		PEACH: {
			onMerge: function onMerge(f1, f2, x, y) {
				LK.getSound('stonks').play();
				return self.standardMerge(f1, f2, x, y);
			}
		},
		COCONUT: {
			onMerge: function onMerge(f1, f2, x, y) {
				LK.getSound('ThisIsFine').play();
				return self.standardMerge(f1, f2, x, y);
			},
			onSpawn: function onSpawn() {
				LK.getSound('stonks').play();
			}
		},
		DURIAN: {
			onMerge: function onMerge(f1, f2, x, y) {
				LK.setScore(LK.getScore() + f1.type.points);
				updateScoreDisplay(); // Assumes this global function exists
				removeFruitFromGame(f1); // Assumes this global function exists
				removeFruitFromGame(f2); // Assumes this global function exists
				releasePineappleOnMerge(); // Assumes this global function exists
				return null; // No new fruit created from Durian merge
			}
		}
	};
	self.getMergeHandler = function (fruitTypeId) {
		if (!fruitTypeId) {
			console.warn("FruitBehavior: fruitTypeId is null or undefined in getMergeHandler. Defaulting to CHERRY.");
			return self.behaviors.CHERRY;
		}
		var upperTypeId = fruitTypeId.toUpperCase();
		return self.behaviors[upperTypeId] || self.behaviors.CHERRY;
	};
	self.standardMerge = function (fruit1, fruit2, posX, posY) {
		var nextFruitKey = fruit1.type.next;
		if (!nextFruitKey) {
			console.error("FruitBehavior: 'next' type is undefined for fruit type: " + fruit1.type.id);
			return null; // Cannot create next level fruit
		}
		var nextType = FruitTypes[nextFruitKey.toUpperCase()];
		if (!nextType) {
			console.error("FruitBehavior: Next fruit type '" + nextFruitKey + "' not found in FruitTypes.");
			return null;
		}
		releasePineappleOnMerge(); // Assumes this global function exists
		return self.createNextLevelFruit(fruit1, nextType, posX, posY);
	};
	self.createNextLevelFruit = function (sourceFruit, nextType, posX, posY) {
		var newFruit = new Fruit(nextType); // Fruit class constructor
		newFruit.x = posX;
		newFruit.y = posY;
		// Initial scale for merge animation
		newFruit.scaleX = 0.5;
		newFruit.scaleY = 0.5;
		game.addChild(newFruit); // Assumes 'game' is a global Application or Container
		fruits.push(newFruit); // Assumes 'fruits' is a global array
		if (spatialGrid) {
			// Assumes 'spatialGrid' is a global instance
			spatialGrid.insertObject(newFruit);
		}
		LK.setScore(LK.getScore() + nextType.points);
		updateScoreDisplay(); // Assumes this global function exists
		tween(newFruit, {
			scaleX: 1,
			scaleY: 1
		}, {
			duration: 300,
			// Standard merge pop-in duration
			easing: tween.bounceOut
		});
		return newFruit;
	};
	self.playSoundEffect = function (soundId) {
		if (soundId) {
			LK.getSound(soundId).play();
		}
	};
	return self;
});
var Line = Container.expand(function () {
	var self = Container.call(this);
	var lineGraphics = self.attachAsset('floor', {
		// Re-using floor asset for the line
		anchorX: 0.5,
		anchorY: 0.5
	});
	lineGraphics.tint = 0xff0000; // Red color for game over line
	lineGraphics.height = 20; // Make it visually a line
	// Width will be scaled in setupBoundaries
	return self;
});
var MergeComponent = Container.expand(function () {
	var self = Container.call(this);
	self.merging = false; // This property seems to belong to the Fruit itself, not the component instance.
	// Each fruit will have its own 'merging' flag.
	self.fruitBehavior = new FruitBehavior(); // To get specific merge behaviors
	self.beginMerge = function (fruit1, fruit2) {
		// Prevent merging if either fruit is already involved in a merge
		if (fruit1.merging || fruit2.merging) {
			return;
		}
		// Prevent merging if fruit types are different (should be handled by collision check before calling this)
		if (fruit1.type.id !== fruit2.type.id) {
			console.warn("MergeComponent: Attempted to merge fruits of different types. This should be checked earlier.");
			return;
		}
		fruit1.merging = true;
		fruit2.merging = true;
		var midX = (fruit1.x + fruit2.x) / 2;
		var midY = (fruit1.y + fruit2.y) / 2;
		self.animateMerge(fruit1, fruit2, midX, midY);
	};
	self.animateMerge = function (fruit1, fruit2, midX, midY) {
		// Animate fruit1 shrinking and fading
		tween(fruit1, {
			alpha: 0,
			scaleX: 0.5,
			scaleY: 0.5
			// Optional: Move towards merge point if desired, though often they are already close
			// x: midX, 
			// y: midY 
		}, {
			duration: GAME_CONSTANTS.MERGE_ANIMATION_DURATION,
			// Use a constant
			easing: tween.easeOut
		});
		// Animate fruit2 shrinking and fading, then complete the merge
		tween(fruit2, {
			alpha: 0,
			scaleX: 0.5,
			scaleY: 0.5
			// x: midX,
			// y: midY
		}, {
			duration: GAME_CONSTANTS.MERGE_ANIMATION_DURATION,
			easing: tween.easeOut,
			onFinish: function onFinish() {
				self.completeMerge(fruit1, fruit2, midX, midY);
			}
		});
	};
	self.completeMerge = function (fruit1, fruit2, midX, midY) {
		LK.getSound('merge').play();
		self.trackMergeAnalytics(fruit1, fruit2); // For game analytics or special logic
		var behaviorHandler = self.fruitBehavior.getMergeHandler(fruit1.type.id);
		var newFruit = null;
		if (behaviorHandler && behaviorHandler.onMerge) {
			newFruit = behaviorHandler.onMerge(fruit1, fruit2, midX, midY);
		} else {
			// Fallback to standard merge if no specific behavior or handler is missing
			console.warn("MergeComponent: No specific onMerge behavior for " + fruit1.type.id + ", or handler missing. Attempting standard merge.");
			var nextFruitKey = fruit1.type.next;
			if (nextFruitKey) {
				var nextType = FruitTypes[nextFruitKey.toUpperCase()];
				if (nextType) {
					newFruit = self.fruitBehavior.createNextLevelFruit(fruit1, nextType, midX, midY);
				} else {
					console.error("MergeComponent: Fallback standard merge failed, next fruit type '" + nextFruitKey + "' not found.");
				}
			} else {
				console.error("MergeComponent: Fallback standard merge failed, 'next' type is undefined for " + fruit1.type.id);
			}
		}
		// Clean up old fruits. This should happen *after* the new fruit is potentially created and added.
		// Durian behavior handles its own removal.
		if (fruit1.type.id.toUpperCase() !== 'DURIAN') {
			removeFruitFromGame(fruit1); // Global removal function
		}
		// Fruit2 is always removed unless it was part of a special non-creating merge (like Durian's partner)
		// The Durian logic already removes both.
		if (fruit2.type.id.toUpperCase() !== 'DURIAN' || fruit1.type.id.toUpperCase() !== 'DURIAN') {
			// If neither was a Durian, or if fruit1 was Durian (so fruit2 is its partner to be removed)
			removeFruitFromGame(fruit2);
		}
	};
	// This function seems to be for tracking specific game states related to merges
	self.trackMergeAnalytics = function (fruit1, fruit2) {
		// Original logic for lastDroppedHasMerged - ensure lastDroppedFruit is a global variable
		var fromReleasedFruits = fruit1.fromChargedRelease || fruit2.fromChargedRelease;
		var isPlayerDroppedFruitMerge = !fromReleasedFruits && (fruit1 === lastDroppedFruit || fruit2 === lastDroppedFruit) && !lastDroppedHasMerged;
		var fruitHasMergeGracePeriod = fruit1.mergeGracePeriodActive || fruit2.mergeGracePeriodActive;
		if (isPlayerDroppedFruitMerge || fruitHasMergeGracePeriod) {
			lastDroppedHasMerged = true;
		}
	};
	return self;
});
var PhysicsComponent = Container.expand(function () {
	var self = Container.call(this);
	// Note: Properties like vx, vy, gravity, etc., are on the Fruit object itself.
	// This component's methods will modify those properties on the passed 'fruit' instance.
	self.apply = function (fruit) {
		if (fruit.isStatic || fruit.merging) {
			return; // No physics for static or merging fruits
		}
		// If fruit is sleeping, only perform minimal checks or wake it up if necessary.
		if (fruit.isSleeping) {
			// A sleeping fruit should not move. Velocities should already be zero.
			fruit.vx = 0;
			fruit.vy = 0;
			fruit.angularVelocity = 0;
			// Potentially check for significant external forces that might wake it up (handled in collision response)
			return;
		}
		var prevVx = fruit.vx;
		var prevVy = fruit.vy;
		// 1. Apply Gravity
		// fruit.gravity is pre-calculated on fruit creation based on its level
		fruit.vy += fruit.gravity * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT; // Assuming a fixed timestep or adjusting for it
		// 2. Apply Air Friction / Damping (if not in contact with ground)
		if (!fruit._boundaryContacts || !fruit._boundaryContacts.floor) {
			fruit.vx *= GAME_CONSTANTS.AIR_FRICTION_MULTIPLIER;
			fruit.vy *= GAME_CONSTANTS.AIR_FRICTION_MULTIPLIER; // Air friction also affects vertical, less than gravity though
			fruit.angularVelocity *= GAME_CONSTANTS.AIR_ANGULAR_DAMPING;
		} else {
			// If on ground, specific ground friction is handled in CollisionComponent.checkFloorCollision
			// and here for angular damping.
			fruit.angularVelocity *= GAME_CONSTANTS.GROUND_CONTACT_ANGULAR_DAMPING;
		}
		// 3. Update Rotation
		fruit.rotation += fruit.angularVelocity * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT;
		self.handleRotationLimits(fruit); // Cap angular velocity and handle near-zero stopping
		// 4. Limit Velocities (Max Speeds)
		var maxLinearSpeed = GAME_CONSTANTS.MAX_LINEAR_VELOCITY_BASE - getFruitLevel(fruit) * GAME_CONSTANTS.MAX_LINEAR_VELOCITY_REDUCTION_PER_LEVEL;
		maxLinearSpeed = Math.max(GAME_CONSTANTS.MIN_MAX_LINEAR_VELOCITY, maxLinearSpeed);
		var currentSpeed = Math.sqrt(fruit.vx * fruit.vx + fruit.vy * fruit.vy);
		if (currentSpeed > maxLinearSpeed) {
			var ratio = maxLinearSpeed / currentSpeed;
			fruit.vx *= ratio;
			fruit.vy *= ratio;
		}
		// 5. Update Position
		fruit.x += fruit.vx * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT;
		fruit.y += fruit.vy * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT;
		// 6. Stabilization and Sleep Logic (Simplified)
		self.checkStabilization(fruit, prevVx, prevVy);
		// Ensure mid-air fruits are not marked as sleeping or stabilized
		if (!fruit._boundaryContacts || !fruit._boundaryContacts.floor && !fruit._boundaryContacts.left && !fruit._boundaryContacts.right) {
			fruit.isFullyStabilized = false;
			fruit.isSleeping = false;
			fruit._sleepCounter = 0;
		}
		// If the fruit is now fully stabilized (but not yet sleeping), force velocities to zero.
		if (fruit.isFullyStabilized && !fruit.isSleeping) {
			if (Math.abs(fruit.vx) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD && Math.abs(fruit.vy) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD && Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.STABLE_ANGULAR_VELOCITY_THRESHOLD) {
				// This is a soft "almost stopped" state, let's push it to fully stopped
				fruit.vx = 0;
				fruit.vy = 0;
				fruit.angularVelocity = 0;
			}
		}
		// Ensure minimum fall speed if in air and moving downwards very slowly (prevents floating)
		if (fruit.vy > 0 && fruit.vy < GAME_CONSTANTS.MIN_FALL_SPEED_IF_SLOWING && (!fruit._boundaryContacts || !fruit._boundaryContacts.floor)) {
			fruit.vy = GAME_CONSTANTS.MIN_FALL_SPEED_IF_SLOWING;
		}
	};
	self.handleRotationLimits = function (fruit) {
		// Cap angular velocity
		var maxAngVel = GAME_CONSTANTS.MAX_ANGULAR_VELOCITY_BASE - getFruitLevel(fruit) * GAME_CONSTANTS.MAX_ANGULAR_VELOCITY_REDUCTION_PER_LEVEL;
		maxAngVel = Math.max(GAME_CONSTANTS.MIN_MAX_ANGULAR_VELOCITY, maxAngVel);
		fruit.angularVelocity = Math.max(-maxAngVel, Math.min(maxAngVel, fruit.angularVelocity));
		// If angular velocity is very low, and fruit is somewhat stable, stop rotation
		if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.ANGULAR_STOP_THRESHOLD_PHYSICS) {
			// Additional check: if linear velocity is also very low, it's more likely to stop rotating
			var linSpeedSq = fruit.vx * fruit.vx + fruit.vy * fruit.vy;
			if (linSpeedSq < GAME_CONSTANTS.LINEAR_SPEED_SQ_FOR_ANGULAR_STOP) {
				fruit.angularVelocity = 0;
				fruit.rotationRestCounter = (fruit.rotationRestCounter || 0) + 1;
			} else {
				fruit.rotationRestCounter = 0;
			}
		} else {
			fruit.rotationRestCounter = 0;
		}
	};
	self.checkStabilization = function (fruit, prevVx, prevVy) {
		// Calculate change in velocity (kinetic energy proxy)
		var deltaVSq = (fruit.vx - prevVx) * (fruit.vx - prevVx) + (fruit.vy - prevVy) * (fruit.vy - prevVy);
		var currentSpeedSq = fruit.vx * fruit.vx + fruit.vy * fruit.vy;
		var currentAngularSpeed = Math.abs(fruit.angularVelocity);
		// Conditions for considering a fruit "active" (not stabilizing)
		var isActive = currentSpeedSq > GAME_CONSTANTS.ACTIVE_LINEAR_SPEED_SQ_THRESHOLD || currentAngularSpeed > GAME_CONSTANTS.ACTIVE_ANGULAR_SPEED_THRESHOLD || deltaVSq > GAME_CONSTANTS.ACTIVE_DELTA_V_SQ_THRESHOLD;
		// Fruit is considered potentially stabilizing if it's in contact with something (floor or walls)
		var onSurface = fruit._boundaryContacts && (fruit._boundaryContacts.floor || fruit._boundaryContacts.left || fruit._boundaryContacts.right);
		if (!isActive && onSurface) {
			fruit._sleepCounter = (fruit._sleepCounter || 0) + 1;
		} else {
			// If it becomes active or is in mid-air, reset counter and wake it up
			fruit._sleepCounter = 0;
			fruit.isSleeping = false;
			fruit.isFullyStabilized = false; // If active, it's not stabilized
		}
		if (fruit._sleepCounter >= GAME_CONSTANTS.SLEEP_DELAY_FRAMES_SIMPLIFIED) {
			// If it has been inactive for enough frames
			if (!fruit.isSleeping) {
				// Only set to sleeping if not already
				fruit.isSleeping = true;
				fruit.isFullyStabilized = true; // Sleeping implies fully stabilized
				fruit.vx = 0;
				fruit.vy = 0;
				fruit.angularVelocity = 0;
			}
		} else if (fruit._sleepCounter >= GAME_CONSTANTS.STABILIZED_DELAY_FRAMES) {
			// If inactive for a shorter period, it's "fully stabilized" but not yet sleeping
			// This state can be used for game logic like game over checks
			if (!fruit.isFullyStabilized && !fruit.isSleeping) {
				fruit.isFullyStabilized = true;
				// Don't zero out velocities here forcefully, let the physics system settle them.
				// If they don't settle, the !isActive check will fail next frame.
			}
		} else {
			// If not meeting sleep or stabilized counts, it's not in these states
			fruit.isSleeping = false;
			fruit.isFullyStabilized = false;
		}
	};
	return self;
});
var SpatialGrid = Container.expand(function (cellSize) {
	var self = Container.call(this);
	self.cellSize = cellSize || 200; // Default cell size
	self.grid = {}; // Stores cellKey: [object1, object2, ...]
	self.lastRebuildTime = Date.now();
	self.rebuildInterval = GAME_CONSTANTS.SPATIAL_GRID_REBUILD_INTERVAL_MS; // How often to rebuild if objects don't move
	self.insertObject = function (obj) {
		if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number' || obj.merging || obj.isStatic) {
			return; // Skip invalid, merging, or static objects
		}
		obj._currentCells = obj._currentCells || []; // Ensure property exists
		var cells = self.getCellsForObject(obj);
		obj._currentCells = cells.slice(); // Store the cells the object is currently in
		for (var i = 0; i < cells.length; i++) {
			var cellKey = cells[i];
			if (!self.grid[cellKey]) {
				self.grid[cellKey] = [];
			}
			// Avoid duplicates if somehow inserted twice
			if (self.grid[cellKey].indexOf(obj) === -1) {
				self.grid[cellKey].push(obj);
			}
		}
	};
	self.removeObject = function (obj) {
		if (!obj || !obj._currentCells) {
			// Check if obj is valid and has _currentCells
			return;
		}
		var cells = obj._currentCells; // Use stored cells for removal efficiency
		for (var i = 0; i < cells.length; i++) {
			var cellKey = cells[i];
			if (self.grid[cellKey]) {
				var cellIndex = self.grid[cellKey].indexOf(obj);
				if (cellIndex !== -1) {
					self.grid[cellKey].splice(cellIndex, 1);
				}
				// Clean up empty cell arrays
				if (self.grid[cellKey].length === 0) {
					delete self.grid[cellKey];
				}
			}
		}
		obj._currentCells = []; // Clear stored cells
	};
	// Calculates which grid cells an object overlaps with
	self.getCellsForObject = function (obj) {
		if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number') {
			return []; // Return empty array for invalid input
		}
		var cells = [];
		// Using object's center and dimensions to find cell range
		var halfWidth = obj.width / 2;
		var halfHeight = obj.height / 2;
		var minCellX = Math.floor((obj.x - halfWidth) / self.cellSize);
		var maxCellX = Math.floor((obj.x + halfWidth) / self.cellSize);
		var minCellY = Math.floor((obj.y - halfHeight) / self.cellSize);
		var maxCellY = Math.floor((obj.y + halfHeight) / self.cellSize);
		for (var cellX = minCellX; cellX <= maxCellX; cellX++) {
			for (var cellY = minCellY; cellY <= maxCellY; cellY++) {
				cells.push(cellX + "," + cellY); // Unique key for each cell
			}
		}
		return cells;
	};
	self.updateObject = function (obj) {
		if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number' || obj.merging || obj.isStatic) {
			// Don't update static or merging objects in the grid
			return;
		}
		var newCells = self.getCellsForObject(obj);
		var oldCells = obj._currentCells || [];
		// Efficiently check if cells have changed
		var cellsChanged = false;
		if (oldCells.length !== newCells.length) {
			cellsChanged = true;
		} else {
			// Check if all new cells are in oldCells (and vice-versa implied by same length)
			for (var i = 0; i < newCells.length; i++) {
				if (oldCells.indexOf(newCells[i]) === -1) {
					cellsChanged = true;
					break;
				}
			}
		}
		if (cellsChanged) {
			self.removeObject(obj); // Remove from old cells
			self.insertObject(obj); // Insert into new cells
		}
	};
	// Gets potential collision candidates for an object
	self.getPotentialCollisions = function (obj) {
		var candidates = [];
		if (!obj || typeof obj.x !== 'number') {
			return candidates;
		} // Basic check for valid object
		var cells = self.getCellsForObject(obj); // Determine cells the object is in
		var addedObjects = {}; // To prevent duplicate candidates if an object is in multiple shared cells
		for (var i = 0; i < cells.length; i++) {
			var cellKey = cells[i];
			if (self.grid[cellKey]) {
				for (var j = 0; j < self.grid[cellKey].length; j++) {
					var otherObj = self.grid[cellKey][j];
					// Ensure otherObj is valid, not the same as obj, and not already added
					if (otherObj && otherObj !== obj && !addedObjects[otherObj.id]) {
						if (otherObj.merging || otherObj.isStatic) {
							continue;
						} // Skip merging or static candidates
						candidates.push(otherObj);
						addedObjects[otherObj.id] = true; // Mark as added
					}
				}
			}
		}
		return candidates;
	};
	self.clear = function () {
		self.grid = {};
		self.lastRebuildTime = Date.now();
	};
	// Rebuilds the entire grid. Useful if many objects become invalid or after a reset.
	self.rebuildGrid = function (allObjects) {
		self.grid = {}; // Clear current grid
		self.lastRebuildTime = Date.now();
		if (Array.isArray(allObjects)) {
			for (var i = 0; i < allObjects.length; i++) {
				// Only insert valid, non-merging, non-static objects
				if (allObjects[i] && !allObjects[i].merging && !allObjects[i].isStatic) {
					self.insertObject(allObjects[i]);
				}
			}
		}
	};
	return self;
});
var TrajectoryLine = Container.expand(function () {
	var self = Container.call(this);
	self.dotPool = new DotPool(30); // Reduced default pool size, can grow
	self.activeDots = []; // DisplayObjects currently on stage
	// self.dots renamed to activeDots for clarity
	self.dotSpacing = 25; // Spacing between dots
	self.dotSize = 15; // Visual size (not used in current createObject)
	// Dynamically calculate maxDots so the trajectory can reach from drop point to floor
	var dropActualY = GAME_CONSTANTS.DROP_POINT_Y + GAME_CONSTANTS.DROP_START_Y_OFFSET;
	var distanceToCover = GAME_CONSTANTS.GAME_HEIGHT - dropActualY;
	self.maxDots = Math.ceil(distanceToCover / self.dotSpacing) + 5; // +5 for a little buffer
	if (self.maxDots <= 0) self.maxDots = 60; // Fallback if calculation is off
	self.createDots = function () {
		// Renamed from initialize for clarity
		self.clearDots(); // Clear existing before creating new pool (if any)
		// DotPool's initialize is called by its constructor if initialSize is given
	};
	self.clearDots = function () {
		while (self.activeDots.length > 0) {
			var dot = self.activeDots.pop();
			if (dot) {
				self.removeChild(dot); // Remove from PIXI stage
				self.dotPool.release(dot); // Return to pool
			}
		}
	};
	self.updateTrajectory = function (startX, startY) {
		if (!activeFruit) {
			// Assumes activeFruit is a global variable for the fruit being aimed
			self.clearDots(); // Clear if no active fruit
			return;
		}
		self.clearDots(); // Clear previous trajectory
		var currentY = startY;
		var dotCount = 0;
		var hitDetected = false;
		var currentFruitRadius = activeFruit.width / 2; // Use current active fruit's size
		// Reduce the number of fine-grained collision checks for trajectory dots
		var COLLISION_CHECK_INTERVAL = 3; // Only check every 3rd dot for collisions
		while (dotCount < self.maxDots && !hitDetected) {
			var dot = self.dotPool.get();
			if (!dot) {
				continue;
			} // Should not happen if pool can grow or is pre-filled
			self.addChild(dot);
			self.activeDots.push(dot);
			dot.x = startX;
			dot.y = currentY;
			dot.visible = true;
			dot.alpha = 1.0 - dotCount / self.maxDots * 0.7; // Fade out dots further down
			dot.scale.set(1.0 - dotCount / self.maxDots * 0.5); // Make dots smaller further down
			// Check for floor collision
			var floorCollisionY = gameFloor.y - gameFloor.height / 2 - currentFruitRadius; // Adjusted for fruit radius
			if (currentY > floorCollisionY) {
				if (self.activeDots.length > 0) {
					// Adjust last dot to touch floor
					self.activeDots[self.activeDots.length - 1].y = floorCollisionY;
				}
				hitDetected = true;
				break; // Stop trajectory at floor
			}
			// Only check for fruit collisions every COLLISION_CHECK_INTERVAL dots
			if (dotCount % COLLISION_CHECK_INTERVAL === 0) {
				// Create a temporary bounding box for the current trajectory point
				var trajectoryCheckObject = {
					x: startX,
					y: currentY,
					width: activeFruit.width,
					// Use active fruit's dimensions for check
					height: activeFruit.height,
					id: 'trajectory_check_point' // Unique ID for spatial grid
				};
				var potentialHits = spatialGrid.getPotentialCollisions(trajectoryCheckObject);
				for (var j = 0; j < potentialHits.length; j++) {
					var fruit = potentialHits[j];
					// Ensure fruit is valid and not the active fruit itself, and not merging/static
					if (fruit && fruit !== activeFruit && !fruit.merging && !fruit.isStatic && fruit.width && fruit.height) {
						// Simple AABB intersection check for trajectory prediction
						if (self.wouldIntersectFruit(startX, currentY, activeFruit, fruit)) {
							if (self.activeDots.length > 0) {
								// Adjust the last dot's position to be just before the collision
								// This is a simplification; true surface point is harder.
								// We can estimate by moving it back slightly along the drop path.
								var lastDot = self.activeDots[self.activeDots.length - 1];
								lastDot.y = fruit.y - fruit.height / 2 - activeFruit.height / 2 - 2; // Place above hit fruit
								if (lastDot.y > floorCollisionY) {
									lastDot.y = floorCollisionY;
								} // Don't go below floor
							}
							hitDetected = true;
							break;
						}
					}
				}
				if (hitDetected) {
					break;
				}
			}
			currentY += self.dotSpacing;
			dotCount++;
		}
	};
	// Simplified intersection check for trajectory (AABB based)
	self.wouldIntersectFruit = function (dropX, dropY, activeFruitObj, targetFruitObj) {
		var activeHalfWidth = activeFruitObj.width / 2;
		var activeHalfHeight = activeFruitObj.height / 2;
		var targetHalfWidth = targetFruitObj.width / 2;
		var targetHalfHeight = targetFruitObj.height / 2;
		// AABB check:
		var collidesX = Math.abs(dropX - targetFruitObj.x) < activeHalfWidth + targetHalfWidth;
		var collidesY = Math.abs(dropY - targetFruitObj.y) < activeHalfHeight + targetHalfHeight;
		return collidesX && collidesY;
	};
	return self;
});
/**** 
* Initialize Game
****/ 
var game = new LK.Game({
	backgroundColor: 0xffe122
});
/**** 
* Game Code
****/ 
// --- Constants ---
var _GAME_CONSTANTS; // Keep original babel helper structure if present
function _typeof(o) {
	/* Babel helper */"@babel/helpers - typeof";
	return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
		return typeof o;
	} : function (o) {
		return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
	}, _typeof(o);
}
function _defineProperty(e, r, t) {
	/* Babel helper */ 
	return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
		value: t,
		enumerable: !0,
		configurable: !0,
		writable: !0
	}) : e[r] = t, e;
}
function _toPropertyKey(t) {
	/* Babel helper */ 
	var i = _toPrimitive(t, "string");
	return "symbol" == _typeof(i) ? i : i + "";
}
function _toPrimitive(t, r) {
	/* Babel helper */ 
	if ("object" != _typeof(t) || !t) {
		return t;
	}
	var e = t[Symbol.toPrimitive];
	if (void 0 !== e) {
		var i = e.call(t, r || "default");
		if ("object" != _typeof(i)) {
			return i;
		}
		throw new TypeError("@@toPrimitive must return a primitive value.");
	}
	return ("string" === r ? String : Number)(t);
}
// Refactored and added constants
var GAME_CONSTANTS = _GAME_CONSTANTS = {
	GAME_WIDTH: 2048,
	GAME_HEIGHT: 2732,
	DROP_POINT_Y: 200,
	DROP_START_Y_OFFSET: 200,
	// Initial offset above the drop point for visual placement
	CLICK_DELAY_MS: 200,
	// Reduced for responsiveness
	FRUIT_IMMUNITY_MS: 800,
	// Shorter immunity after drop
	MERGE_GRACE_MS: 1500,
	// Shorter grace period for merges
	MERGE_ANIMATION_DURATION: 150,
	// ms for merge shrink animation
	GAME_OVER_LINE_Y: 550,
	GAME_OVER_COUNTDOWN_MS: 2500,
	// Slightly shorter countdown
	// --- Simplified Physics Constants ---
	BASE_GRAVITY: 1,
	// Increased gravity for faster falling fruits
	// Adjusted for a different feel, may need tuning with TIMESTEP
	GRAVITY_LEVEL_POWER_SCALE: 1.3,
	// How much fruit level affects its mass/gravity effect (power)
	GRAVITY_LEVEL_MULTIPLIER_ADJUSTED: 0.15,
	// Additional multiplier for level-based gravity adjustment
	PHYSICS_TIMESTEP_ADJUSTMENT: 1.0,
	// If using fixed update, this is 1. If variable, use delta time. For now, 1.
	BASE_ELASTICITY: 0.4,
	// Base bounciness
	MIN_ELASTICITY: 0.1,
	// Minimum bounciness for heaviest fruits
	ELASTICITY_DECREASE_PER_LEVEL: 0.02,
	// How much elasticity decreases per fruit level
	AIR_FRICTION_MULTIPLIER: 0.99,
	// General damping for linear velocity in air (per frame)
	GROUND_LINEAR_FRICTION: 0.92,
	// Friction for linear velocity when on ground (per frame by CollisionComponent)
	AIR_ANGULAR_DAMPING: 0.98,
	// Damping for angular velocity in air
	GROUND_CONTACT_ANGULAR_DAMPING: 0.92,
	// General angular damping when on ground
	GROUND_ANGULAR_DAMPING: 0.85,
	// Specific angular damping from floor contact (CollisionComponent)
	WALL_BOUNCE_DAMPING: 0.85,
	// How much velocity is retained on wall bounce (multiplied by elasticity)
	FLOOR_BOUNCE_DAMPING: 0.75,
	// How much velocity is retained on floor bounce
	WALL_ANGULAR_IMPULSE_FACTOR: 0,
	// Small rotation from hitting walls
	WALL_ANGULAR_DAMPING: 0.9,
	// Damping of angular velocity after wall hit
	GROUND_ROLLING_FACTOR: 0.008,
	// How much horizontal speed contributes to rolling on ground
	MIN_VX_FOR_ROLLING: 0.1,
	// Minimum horizontal speed to initiate rolling effect
	MAX_LINEAR_VELOCITY_BASE: 50,
	MAX_LINEAR_VELOCITY_REDUCTION_PER_LEVEL: 2.0,
	MIN_MAX_LINEAR_VELOCITY: 10,
	MAX_ANGULAR_VELOCITY_BASE: 0.12,
	// Radians per frame
	MAX_ANGULAR_VELOCITY_REDUCTION_PER_LEVEL: 0.005,
	MIN_MAX_ANGULAR_VELOCITY: 0.03,
	ANGULAR_STOP_THRESHOLD_PHYSICS: 0.0008,
	// If angular speed is below this, might stop (in PhysicsComponent)
	LINEAR_SPEED_SQ_FOR_ANGULAR_STOP: 0.05,
	// If linear speed squared is also low, rotation fully stops
	RESTING_VELOCITY_THRESHOLD: 0.4,
	// Below this vy on floor, vy becomes 0 (in CollisionComponent)
	ANGULAR_RESTING_THRESHOLD: 0.005,
	// Below this angularV on floor, angularV becomes 0 (in CollisionComponent)
	MAX_WALL_CONTACT_FRAMES_FOR_FRICTION_EFFECT: 10,
	// Max frames wall contact has minor effect (used by CollisionComponent state)
	// Simplified Sleep/Stabilization State Constants
	SLEEP_DELAY_FRAMES_SIMPLIFIED: 45,
	// Frames of low activity to go to sleep
	STABILIZED_DELAY_FRAMES: 20,
	// Frames of low activity to be considered "fully stabilized" (for game over, etc.)
	ACTIVE_LINEAR_SPEED_SQ_THRESHOLD: 0.1,
	// If linear speed squared is above this, fruit is active
	ACTIVE_ANGULAR_SPEED_THRESHOLD: 0.005,
	// If angular speed is above this, fruit is active
	ACTIVE_DELTA_V_SQ_THRESHOLD: 0.05,
	// If change in velocity squared is high, fruit is active
	STABLE_VELOCITY_THRESHOLD: 0.05,
	// If velocities are below this when 'isFullyStabilized', they are zeroed out.
	STABLE_ANGULAR_VELOCITY_THRESHOLD: 0.001,
	MIN_FALL_SPEED_IF_SLOWING: 0.05,
	// Prevents fruits from floating if gravity effect becomes too small mid-air
	WAKE_UP_IMPULSE_THRESHOLD_LINEAR: 0.5,
	// Min linear impulse from collision to wake up a sleeping fruit
	WAKE_UP_IMPULSE_THRESHOLD_ANGULAR: 0.01,
	// Min angular impulse from collision to wake up
	// Collision Response
	FRUIT_COLLISION_SEPARATION_FACTOR: 0.3,
	// How much to push fruits apart on overlap (0 to 1)
	FRUIT_COLLISION_BASE_MASS_POWER: 1.5,
	// Fruit mass = level ^ this_power (for impulse calc)
	FRUIT_COLLISION_FRICTION_COEFFICIENT: 0.1,
	// Tangential friction between fruits
	FRUIT_COLLISION_ROTATION_TRANSFER: 0.002,
	// How much tangential collision affects rotation (keep small)
	FRUIT_HITBOX_REDUCTION_PER_LEVEL_DIFF: 1.5,
	// For `getAdjustedFruitRadius`
	BOUNCE_SOUND_VELOCITY_THRESHOLD: 1.0,
	// Adjusted threshold for playing bounce sound
	// Spatial Grid
	SPATIAL_GRID_REBUILD_INTERVAL_MS: 30000,
	// Less frequent full rebuilds
	SPATIAL_GRID_CELL_SIZE_FACTOR: 1.0,
	// Base cell size on average fruit size
	// UI & Game Features
	CHARGE_NEEDED_FOR_RELEASE: 15,
	PORTAL_UI_Y: 120,
	PORTAL_UI_X_OFFSET: 870,
	PORTAL_TWEEN_DURATION: 300,
	PORTAL_PULSE_DURATION: 500,
	PINEAPPLE_MERGES_NEEDED: 15,
	PINEAPPLE_START_Y: 200,
	PINEAPPLE_END_POS_FACTOR: 0.16,
	PINEAPPLE_TWEEN_DURATION: 300,
	PINEAPPLE_IMMUNITY_MS: 2000,
	// Slightly shorter
	FIRE_BASE_COUNT: 3,
	FIRE_FRUIT_TYPE_THRESHOLD: 1,
	FIRE_MAX_COUNT: 15,
	FIRE_START_Y_OFFSET: 50,
	FIRE_STACK_Y_OFFSET: 100,
	FIRE_X_SPREAD: 500,
	FIRE_FLICKER_SPEED_BASE: 500,
	FIRE_FLICKER_SPEED_RANDOM: 300,
	FIRE_ALPHA_MIN: 0.1,
	FIRE_ALPHA_MAX: 0.5,
	FIRE_FRAME_DURATION: 200,
	COCONUT_SPAWN_SCORE_INTERVAL: 1000,
	EVOLUTION_LINE_Y: 120,
	EVOLUTION_ICON_MAX_SIZE: 150,
	// Adjusted for potentially smaller screen area
	EVOLUTION_ICON_SPACING: 15,
	SCORE_TEXT_Y: 400,
	// Adjusted for consistency if needed
	SCORE_TEXT_SIZE: 120 // Adjusted for consistency
}; // Removed the comma and the comment "/* Babel definitions if any */"
// --- Game Variables ---
var gameOverLine;
var pineapple; // The special pineapple fruit instance
var pineappleActive = false; // If the special pineapple has been dropped
// var pineapplePushCount = 0; // This seems to be replaced by mergeCounter for pineapple logic
var trajectoryLine; // Instance of TrajectoryLine class
var isClickable = true; // Prevents rapid firing of fruits
var evolutionLine; // UI element showing fruit progression
var fireContainer; // Container for fire particle effects
var activeFireElements = []; // Array of active fire Particle instances
var fruitLevels = {
	// Defines the "level" or tier of each fruit type
	'CHERRY': 1,
	'GRAPE': 2,
	'APPLE': 3,
	'ORANGE': 4,
	'WATERMELON': 5,
	'PINEAPPLE': 6,
	'MELON': 7,
	'PEACH': 8,
	'COCONUT': 9,
	'DURIAN': 10
};
var FruitTypes = {
	// Defines properties for each fruit
	CHERRY: {
		id: 'cherry',
		size: 150,
		points: 1,
		next: 'grape'
	},
	GRAPE: {
		id: 'grape',
		size: 200,
		points: 2,
		next: 'apple'
	},
	APPLE: {
		id: 'apple',
		size: 250,
		points: 3,
		next: 'orange'
	},
	ORANGE: {
		id: 'orange',
		size: 200,
		points: 5,
		next: 'watermelon'
	},
	// Note: Orange size was smaller than apple
	WATERMELON: {
		id: 'watermelon',
		size: 350,
		points: 8,
		next: 'pineapple'
	},
	PINEAPPLE: {
		id: 'pineapple',
		size: 400,
		points: 13,
		next: 'melon'
	},
	MELON: {
		id: 'melon',
		size: 450,
		points: 21,
		next: 'peach'
	},
	PEACH: {
		id: 'peach',
		size: 500,
		points: 34,
		next: 'coconut'
	},
	COCONUT: {
		id: 'coconut',
		size: 550,
		points: 55,
		next: 'durian'
	},
	DURIAN: {
		id: 'durian',
		size: 600,
		points: 89,
		next: null
	} // No next fruit after Durian
};
var gameWidth = GAME_CONSTANTS.GAME_WIDTH;
var gameHeight = GAME_CONSTANTS.GAME_HEIGHT;
var fruits = []; // Array to hold all active Fruit instances in the game
var nextFruitType = null; // The type of the next fruit to be dropped
var activeFruit = null; // The Fruit instance currently being controlled by the player
var wallLeft, wallRight, gameFloor; // Boundary objects
var dropPointY = GAME_CONSTANTS.DROP_POINT_Y; // Y position where fruits are aimed/dropped from
var gameOver = false; // Game state flag
var scoreText; // Text object for displaying score
var isDragging = false; // If the player is currently dragging the active fruit
// Charged Ball UI / Mechanic
var chargedBallUI = null; // Instance of ChargedBallUI
var chargeCounter = 0; // Counts drops towards charged ball release
var readyToReleaseCharged = false; // Flag if the charged ball is ready
// Merge tracking
var mergeCounter = 0; // Counts merges, used for pineapple release
var lastDroppedFruit = null; // Reference to the last fruit dropped by the player
var lastDroppedHasMerged = false; // Flag if the last dropped fruit has successfully merged
var spatialGrid = null; // Instance of SpatialGrid for collision optimization
var lastScoreCheckForCoconut = 0; // Tracks score for spawning coconuts
// --- Helper Functions ---
function getFruitLevel(fruit) {
	if (!fruit || !fruit.type || !fruit.type.id) {
		// console.warn("getFruitLevel: Invalid fruit or fruit type. Defaulting to highest level to be safe.");
		return 10; // Default to a high level if type is unknown, makes it heavy.
	}
	var typeId = fruit.type.id.toUpperCase();
	if (fruitLevels.hasOwnProperty(typeId)) {
		return fruitLevels[typeId];
	}
	// console.warn("getFruitLevel: Unknown fruit type ID '" + typeId + "'. Defaulting.");
	return 10;
}
// Helper to get an "adjusted" radius for collision, potentially smaller for higher level fruits
// This is a simplified version of the original hitbox reduction.
function getAdjustedFruitRadius(fruit) {
	var baseRadius = fruit.width / 2; // Assuming width is representative for radius
	var level = getFruitLevel(fruit);
	// Reduce radius slightly for fruits above a certain level, e.g. level 4+
	// var reduction = Math.max(0, level - 3) * GAME_CONSTANTS.FRUIT_HITBOX_REDUCTION_PER_LEVEL_DIFF;
	// For simplicity now, just use baseRadius or a fixed small reduction if needed.
	// The primary collision logic uses full AABB now, so complex radius adjustment is less critical.
	return baseRadius;
}
// --- Game Logic Functions ---
function releasePineappleOnMerge() {
	mergeCounter++;
	pushPineapple(); // Animates the pineapple appearing
	if (mergeCounter >= GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED && !pineappleActive && pineapple) {
		pineappleActive = true;
		pineapple.isStatic = false; // Allow it to fall
		pineapple.immuneToGameOver = true; // Temporary immunity
		applyDropPhysics(pineapple, 1.5); // Give it a gentle initial push
		fruits.push(pineapple);
		if (spatialGrid) {
			spatialGrid.insertObject(pineapple);
		}
		LK.setTimeout(function () {
			if (pineapple && fruits.indexOf(pineapple) !== -1) {
				// Check if it still exists and is in game
				pineapple.immuneToGameOver = false;
			}
		}, GAME_CONSTANTS.PINEAPPLE_IMMUNITY_MS);
		// Note: setupPineapple() creates the initial static pineapple.
		// Here we are "releasing" it. If a new one is needed, setupPineapple should be called again.
		// For now, let's assume one special pineapple per requirement met.
		mergeCounter = 0; // Reset for next pineapple
	}
}
function setupBoundaries() {
	// Left Wall
	wallLeft = game.addChild(LK.getAsset('wall', {
		anchorX: 0.5,
		anchorY: 0.5
	}));
	wallLeft.x = 0; // Positioned at the left edge
	wallLeft.y = gameHeight / 2;
	wallLeft.alpha = 0; // Invisible, but physics active
	// Right Wall
	wallRight = game.addChild(LK.getAsset('wall', {
		anchorX: 0.5,
		anchorY: 0.5
	}));
	wallRight.x = gameWidth; // Positioned at the right edge
	wallRight.y = gameHeight / 2;
	wallRight.alpha = 0;
	// Floor
	gameFloor = game.addChild(LK.getAsset('floor', {
		anchorX: 0.5,
		anchorY: 0.5
	}));
	gameFloor.x = gameWidth / 2;
	gameFloor.y = gameHeight; // Positioned at the bottom
	gameFloor.alpha = 0;
	// Game Over Line
	gameOverLine = game.addChild(new Line()); // Uses the Line class
	gameOverLine.x = gameWidth / 2;
	gameOverLine.y = GAME_CONSTANTS.GAME_OVER_LINE_Y;
	gameOverLine.scaleX = gameWidth / 100; // Scale to full screen width (Line asset is 100px wide)
	gameOverLine.scaleY = 0.2; // Make it thin
	gameOverLine.alpha = 1; // Visible
}
function createNextFruit() {
	var fruitProbability = Math.random();
	// Determine next fruit type (e.g., mostly cherries, some grapes)
	var typeKey = fruitProbability < 0.65 ? 'CHERRY' : fruitProbability < 0.9 ? 'GRAPE' : 'APPLE'; // Added Apple for variety
	nextFruitType = FruitTypes[typeKey];
	activeFruit = new Fruit(nextFruitType);
	// Position above drop point, centered based on last drop or default
	activeFruit.x = lastDroppedFruit && lastDroppedFruit.x ? lastDroppedFruit.x : gameWidth / 2;
	activeFruit.y = dropPointY - GAME_CONSTANTS.DROP_START_Y_OFFSET + 300; // Start lower to avoid top edge
	activeFruit.isStatic = true; // Static until dropped
	game.addChild(activeFruit);
	if (trajectoryLine) {
		trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y + GAME_CONSTANTS.DROP_START_Y_OFFSET * 2); // Start trajectory from actual drop height
	}
}
function dropFruit() {
	if (gameOver || !activeFruit || !isClickable) {
		return; // Can't drop if game over, no active fruit, or click delay active
	}
	isClickable = false;
	LK.setTimeout(function () {
		isClickable = true;
	}, GAME_CONSTANTS.CLICK_DELAY_MS);
	activeFruit.isStatic = false; // Make it dynamic
	activeFruit.y = dropPointY + GAME_CONSTANTS.DROP_START_Y_OFFSET + 300; // Ensure it starts at the correct drop Y (lowered)
	applyDropPhysics(activeFruit, 2.0); // Apply initial physics forces (forceMultiplier can be tuned)
	fruits.push(activeFruit);
	if (spatialGrid) {
		spatialGrid.insertObject(activeFruit);
	}
	lastDroppedFruit = activeFruit;
	lastDroppedHasMerged = false; // Reset merge tracking for this new fruit
	// Increment charge counter for special ability
	chargeCounter++;
	updateChargedBallDisplay();
	if (chargeCounter >= GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE && !readyToReleaseCharged) {
		// releaseChargedBalls(); // This was the old name, new one is setReadyState on UI
		chargedBallUI && chargedBallUI.setReadyState(true); // Signal UI it's ready
		readyToReleaseCharged = true; // Game logic flag
	}
	// Grant a temporary grace period for merging
	activeFruit.mergeGracePeriodActive = true;
	LK.setTimeout(function () {
		if (activeFruit && fruits.indexOf(activeFruit) !== -1) {
			// Check if fruit still exists
			activeFruit.mergeGracePeriodActive = false;
		}
	}, GAME_CONSTANTS.MERGE_GRACE_MS);
	if (trajectoryLine) {
		// Clear trajectory line after dropping
		trajectoryLine.clearDots();
	}
	// Reset 'fromChargedRelease' flag for all existing fruits (if it was used)
	for (var i = 0; i < fruits.length; i++) {
		if (fruits[i] && fruits[i].fromChargedRelease) {
			fruits[i].fromChargedRelease = false;
		}
	}
	LK.getSound('drop').play();
	// Handle the "charged ball" release (Pickle Rick orange)
	if (readyToReleaseCharged && chargeCounter >= GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE) {
		LK.getSound('pickleRick').play();
		var orange = new Fruit(FruitTypes.ORANGE); // Spawn an orange
		// Determine spawn position for the orange
		var minX = wallLeft.x + wallLeft.width / 2 + orange.width / 2 + 20;
		var maxX = wallRight.x - wallRight.width / 2 - orange.width / 2 - 20;
		orange.x = minX + Math.random() * (maxX - minX);
		orange.y = -orange.height; // Start off-screen from top
		orange.isStatic = false;
		applyDropPhysics(orange, 2.5 + (Math.random() * 1.0 - 0.5)); // Give it a varied drop force
		orange.fromChargedRelease = true; // Mark it as special
		game.addChild(orange);
		fruits.push(orange);
		if (spatialGrid) {
			spatialGrid.insertObject(orange);
		}
		chargeCounter = 0; // Reset charge
		resetChargedBalls(); // Reset UI
		readyToReleaseCharged = false; // Reset flag
	}
	activeFruit = null; // Current fruit is dropped, clear activeFruit
	createNextFruit(); // Prepare the next one
}
function applyDropPhysics(fruit, forceMultiplier) {
	var fruitLevel = getFruitLevel(fruit);
	// Base downward force, slightly adjusted by level (heavier fruits fall a bit 'straighter')
	var levelAdjustedForce = forceMultiplier * (1 - fruitLevel * 0.03); // Small reduction for higher levels
	// Small random horizontal angle for variation
	var angleSpread = 15; // degrees
	var angle = (Math.random() * angleSpread - angleSpread / 2) * (Math.PI / 180); // radians
	fruit.vx = Math.sin(angle) * levelAdjustedForce * 0.5; // Horizontal component is less forceful
	fruit.vy = Math.abs(Math.cos(angle) * levelAdjustedForce); // Ensure initial vy is downwards or zero
	if (fruit.vy < 0.1) {
		fruit.vy = 0.1;
	} // Minimum downward push
	fruit.angularVelocity = Math.random() * 0.02 - 0.01; // Tiny random initial spin
	// Recalculate gravity based on its specific properties if needed, or ensure it's set
	fruit.gravity = GAME_CONSTANTS.BASE_GRAVITY * (1 + Math.pow(fruitLevel, GAME_CONSTANTS.GRAVITY_LEVEL_POWER_SCALE) * GAME_CONSTANTS.GRAVITY_LEVEL_MULTIPLIER_ADJUSTED);
	// Reset physics states
	fruit.isSleeping = false;
	fruit.isFullyStabilized = false;
	fruit._sleepCounter = 0;
	fruit.immuneToGameOver = true; // Temporary immunity
	LK.setTimeout(function () {
		if (fruit && fruits.indexOf(fruit) !== -1) {
			fruit.immuneToGameOver = false;
		}
	}, GAME_CONSTANTS.FRUIT_IMMUNITY_MS);
}
function updateScoreDisplay() {
	if (scoreText) {
		// Ensure scoreText is initialized
		scoreText.setText(LK.getScore());
	}
}
function setupUI() {
	// Score Text
	scoreText = new Text2("0", {
		// Assuming Text2 is a valid class in LK or Upit
		size: GAME_CONSTANTS.SCORE_TEXT_SIZE,
		fill: 0x000000 // Black color
	});
	scoreText.anchor.set(0.5, 0); // Anchor to top-center
	LK.gui.top.addChild(scoreText); // Add to a top GUI layer
	scoreText.y = GAME_CONSTANTS.SCORE_TEXT_Y; // Position from top
	scoreText.x = gameWidth / 2; // Center horizontally
	// Charged Ball Display
	setupChargedBallDisplay();
}
function setupChargedBallDisplay() {
	if (chargedBallUI) {
		// If exists, destroy before creating new
		chargedBallUI.destroy();
	}
	chargedBallUI = new ChargedBallUI();
	game.addChild(chargedBallUI); // Add to main game stage
	// chargedBallUI.initialize() is called within its constructor
}
function updateChargedBallDisplay() {
	if (chargedBallUI) {
		chargedBallUI.updateChargeDisplay(chargeCounter);
	}
}
// function releaseChargedBalls() { // This logic is now more integrated
// 	readyToReleaseCharged = true;
// 	if (chargedBallUI) {
// 		chargedBallUI.setReadyState(true);
// 	}
// }
function resetChargedBalls() {
	if (chargedBallUI) {
		chargedBallUI.reset();
	}
	readyToReleaseCharged = false; // Also reset the game logic flag
	chargeCounter = 0; // Reset the counter itself
	updateChargedBallDisplay(); // Update UI to reflect reset
}
function checkFruitCollisions() {
	// Reset neighbor/surrounded counts for all fruits before collision checks
	for (var k = 0; k < fruits.length; k++) {
		var fruitInstance = fruits[k];
		if (fruitInstance) {
			fruitInstance.neighborContacts = []; // Reset list of direct contacts
			// fruitInstance.surroundedFrames = 0; // This might be better managed by PhysicsComponent based on continuous contact
		}
	}
	outerLoop: for (var i = fruits.length - 1; i >= 0; i--) {
		var fruit1 = fruits[i];
		if (!fruit1 || fruit1 === activeFruit || fruit1.merging || fruit1.isStatic || fruit1.isSleeping) {
			// Sleeping fruits don't initiate active collision response
			continue;
		}
		var candidates = spatialGrid.getPotentialCollisions(fruit1);
		for (var j = 0; j < candidates.length; j++) {
			var fruit2 = candidates[j];
			// Basic checks to ensure fruit2 is valid for collision
			if (!fruit2 || fruit2 === activeFruit || fruit2.merging || fruit2.isStatic || fruit1 === fruit2) {
				continue;
			}
			// Ensure fruit2 is actually in the main fruits array (sanity check)
			if (fruits.indexOf(fruit2) === -1) {
				continue;
			}
			// --- Collision Detection (AABB style for simplicity here, can be circle later) ---
			var f1HalfWidth = fruit1.width / 2;
			var f1HalfHeight = fruit1.height / 2;
			var f2HalfWidth = fruit2.width / 2;
			var f2HalfHeight = fruit2.height / 2;
			var dx = fruit2.x - fruit1.x;
			var dy = fruit2.y - fruit1.y;
			// AABB collision check
			var collidingX = Math.abs(dx) < f1HalfWidth + f2HalfWidth;
			var collidingY = Math.abs(dy) < f1HalfHeight + f2HalfHeight;
			if (collidingX && collidingY) {
				// Wake up sleeping fruits if they are involved in a collision
				if (fruit1.isSleeping) {
					fruit1.wakeUp();
				}
				if (fruit2.isSleeping) {
					fruit2.wakeUp();
				}
				// Track neighbor contacts
				if (fruit1.neighborContacts.indexOf(fruit2.id) === -1) {
					fruit1.neighborContacts.push(fruit2.id);
				}
				if (fruit2.neighborContacts.indexOf(fruit1.id) === -1) {
					fruit2.neighborContacts.push(fruit1.id);
				}
				// --- Merge Check ---
				if (fruit1.type.id === fruit2.type.id) {
					fruit1.merge(fruit2); // Delegate to fruit's merge component
					continue outerLoop; // Critical: restart outer loop as fruit1 (and fruit2) might be gone
				}
				// --- Collision Response (Simplified Impulse-Based) ---
				var distance = Math.sqrt(dx * dx + dy * dy);
				if (distance === 0) {
					// Prevent division by zero if perfectly overlapped
					distance = 0.1;
					dx = 0.1; // Arbitrary small separation
				}
				var normalX = dx / distance;
				var normalY = dy / distance;
				// 1. Separation (Resolve Overlap)
				// Calculate overlap based on AABB - this is a simplification.
				// For circular, it's (r1+r2) - distance. Here, use an estimate.
				var overlapX = f1HalfWidth + f2HalfWidth - Math.abs(dx);
				var overlapY = f1HalfHeight + f2HalfHeight - Math.abs(dy);
				var overlap = Math.min(overlapX, overlapY) * GAME_CONSTANTS.FRUIT_COLLISION_SEPARATION_FACTOR; // Use smaller overlap dimension
				if (overlap > 0) {
					// Distribute separation based on "mass" (derived from level)
					var level1 = getFruitLevel(fruit1);
					var level2 = getFruitLevel(fruit2);
					var mass1 = Math.pow(level1, GAME_CONSTANTS.FRUIT_COLLISION_BASE_MASS_POWER) || 1; // Ensure mass is at least 1
					var mass2 = Math.pow(level2, GAME_CONSTANTS.FRUIT_COLLISION_BASE_MASS_POWER) || 1;
					var totalMass = mass1 + mass2;
					var separation1 = mass2 / totalMass * overlap;
					var separation2 = mass1 / totalMass * overlap;
					fruit1.x -= normalX * separation1;
					fruit1.y -= normalY * separation1;
					fruit2.x += normalX * separation2;
					fruit2.y += normalY * separation2;
				}
				// 2. Impulse (Exchange Momentum)
				var relVx = fruit2.vx - fruit1.vx;
				var relVy = fruit2.vy - fruit1.vy;
				var contactVelocity = relVx * normalX + relVy * normalY;
				if (contactVelocity < 0) {
					// If objects are moving towards each other
					var combinedElasticity = Math.min(fruit1.elasticity, fruit2.elasticity); // Or average, or max
					var impulseMagnitude = -(1 + combinedElasticity) * contactVelocity;
					// Distribute impulse based on mass
					var impulseRatio1 = mass2 / totalMass;
					var impulseRatio2 = mass1 / totalMass;
					fruit1.vx -= impulseMagnitude * normalX * impulseRatio1;
					fruit1.vy -= impulseMagnitude * normalY * impulseRatio1;
					fruit2.vx += impulseMagnitude * normalX * impulseRatio2;
					fruit2.vy += impulseMagnitude * normalY * impulseRatio2;
					// Wake up fruits if impulse is significant
					if (Math.abs(impulseMagnitude * impulseRatio1) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_LINEAR) {
						fruit1.wakeUp();
					}
					if (Math.abs(impulseMagnitude * impulseRatio2) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_LINEAR) {
						fruit2.wakeUp();
					}
					// 3. Friction (Tangential Impulse) - Simplified
					var tangentX = -normalY;
					var tangentY = normalX;
					var tangentVelocity = relVx * tangentX + relVy * tangentY;
					var frictionImpulseMag = -tangentVelocity * GAME_CONSTANTS.FRUIT_COLLISION_FRICTION_COEFFICIENT;
					fruit1.vx -= frictionImpulseMag * tangentX * impulseRatio1;
					fruit1.vy -= frictionImpulseMag * tangentY * impulseRatio1;
					fruit2.vx += frictionImpulseMag * tangentX * impulseRatio2;
					fruit2.vy += frictionImpulseMag * tangentY * impulseRatio2;
					// 4. Rotational Impulse (very simplified, can be made more complex if needed)
					// This makes fruits spin a little on glancing blows
					var angularImpulse = tangentVelocity * GAME_CONSTANTS.FRUIT_COLLISION_ROTATION_TRANSFER;
					// Distribute angular impulse (inverse of linear mass ratio for inertia approximation)
					// This is a rough approximation. True rotational inertia is more complex.
					var angImpRatio1 = impulseRatio2; // Fruit with less "linear push back" gets more spin
					var angImpRatio2 = impulseRatio1;
					fruit1.angularVelocity -= angularImpulse * angImpRatio1;
					fruit2.angularVelocity += angularImpulse * angImpRatio2;
					if (Math.abs(angularImpulse * angImpRatio1) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_ANGULAR) {
						fruit1.wakeUp();
					}
					if (Math.abs(angularImpulse * angImpRatio2) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_ANGULAR) {
						fruit2.wakeUp();
					}
					// 5. Rotational Friction (Frictional force to reduce relative angular velocities)
					var angularDifference = fruit1.angularVelocity - fruit2.angularVelocity;
					var frictionStrength = 0.02; // Tune this value for desired effect
					fruit1.angularVelocity -= angularDifference * frictionStrength;
					fruit2.angularVelocity += angularDifference * frictionStrength;
					// Optionally, apply a little extra damping when in contact
					fruit1.angularVelocity *= 0.98;
					fruit2.angularVelocity *= 0.98;
				}
			}
		}
	}
}
function checkGameOver() {
	if (gameOver) {
		return;
	}
	for (var i = 0; i < fruits.length; i++) {
		var fruit = fruits[i];
		if (!fruit || fruit === activeFruit || fruit.merging || fruit.isStatic || fruit.immuneToGameOver) {
			continue;
		}
		// Calculate effective top Y of the fruit, considering rotation (simplified)
		var fruitHalfHeight = fruit.height / 2;
		var cosAngle = Math.abs(Math.cos(fruit.rotation));
		var sinAngle = Math.abs(Math.sin(fruit.rotation)); // Using width for sin component of height projection
		var effectiveProjectedHeight = fruitHalfHeight * cosAngle + fruit.width / 2 * sinAngle;
		var fruitTopY = fruit.y - effectiveProjectedHeight;
		if (fruitTopY <= GAME_CONSTANTS.GAME_OVER_LINE_Y) {
			// A fruit is above or touching the game over line.
			// Now check if it's "stable" or "settled" there.
			// Stable means low velocity AND either fully stabilized or sleeping.
			var isMovingSlowly = Math.abs(fruit.vy) < 0.5 && Math.abs(fruit.vx) < 0.5; // Tune these thresholds
			var isConsideredSettled = isMovingSlowly && (fruit.isFullyStabilized || fruit.isSleeping);
			// If it's also somewhat still (e.g., low vy or stabilized state)
			if (isConsideredSettled || fruit.wallContactFrames > 0 && isMovingSlowly) {
				// Wall contact implies it might be stuck
				if (!fruit.gameOverTimer) {
					// Start timer if not already started
					fruit.gameOverTimer = Date.now();
					// Optional: Visual feedback like blinking
					tween(fruit, {
						alpha: 0.5
					}, {
						duration: 200,
						yoyo: true,
						repeat: 5,
						easing: tween.easeInOut
					});
				}
				// Check if timer has exceeded countdown
				if (Date.now() - fruit.gameOverTimer >= GAME_CONSTANTS.GAME_OVER_COUNTDOWN_MS) {
					gameOver = true;
					LK.showGameOver(); // Assumes LK provides this
					return; // Exit loop and function
				}
			} else {
				// If fruit is above line but moving significantly, reset its timer
				if (fruit.gameOverTimer) {
					fruit.gameOverTimer = null;
					fruit.alpha = 1.0; // Reset visual feedback
				}
			}
		} else {
			// Fruit is below the line, reset its timer if it had one
			if (fruit.gameOverTimer) {
				fruit.gameOverTimer = null;
				fruit.alpha = 1.0; // Reset visual feedback
			}
		}
	}
}
function setupPineapple() {
	if (pineapple) {
		pineapple.destroy();
	} // Remove old one if exists
	pineapple = new Fruit(FruitTypes.PINEAPPLE);
	pineapple.x = -pineapple.width; // Start off-screen left
	pineapple.y = GAME_CONSTANTS.PINEAPPLE_START_Y;
	pineapple.isStatic = true; // Static until activated
	pineappleActive = false; // Not yet dropped
	// pineapplePushCount = 0; // Reset if this counter is used elsewhere
	game.addChild(pineapple);
}
function pushPineapple() {
	// Animates the pineapple moving into view
	if (!pineappleActive && pineapple) {
		// Only if not yet dropped and exists
		var progress = Math.min(mergeCounter / GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED, 1.0);
		var startX = -pineapple.width; // Off-screen left
		var targetX = gameWidth * GAME_CONSTANTS.PINEAPPLE_END_POS_FACTOR; // Target resting X
		var newX = startX + progress * (targetX - startX);
		tween(pineapple, {
			x: newX
		}, {
			duration: GAME_CONSTANTS.PINEAPPLE_TWEEN_DURATION,
			easing: tween.bounceOut // Or a smoother ease like tween.easeOut
		});
	}
}
function initGame() {
	LK.setScore(0);
	gameOver = false;
	// Clear existing fruits
	for (var i = fruits.length - 1; i >= 0; i--) {
		if (fruits[i]) {
			if (spatialGrid) {
				spatialGrid.removeObject(fruits[i]);
			}
			fruits[i].destroy();
		}
	}
	fruits = [];
	// Reset UI and game state variables
	if (chargedBallUI) {
		chargedBallUI.destroy();
	} // Destroy if exists
	chargedBallUI = null; // Ensure it's null before recreating
	chargeCounter = 0;
	readyToReleaseCharged = false;
	lastScoreCheckForCoconut = 0;
	lastDroppedFruit = null;
	lastDroppedHasMerged = false;
	mergeCounter = 0;
	isClickable = true;
	// Clear fire elements
	if (fireContainer) {
		for (var i = activeFireElements.length - 1; i >= 0; i--) {
			if (activeFireElements[i]) {
				activeFireElements[i].destroy();
			}
		}
		fireContainer.destroy(); // Destroy the container itself
	}
	fireContainer = new Container(); // Create a new one
	game.addChildAt(fireContainer, 0); // Add to bottom of display stack
	activeFireElements = [];
	// Initialize Spatial Grid
	if (spatialGrid) {
		spatialGrid.clear();
	} else {
		var avgFruitSize = 0;
		var fruitTypeCount = 0;
		for (var typeKey in FruitTypes) {
			avgFruitSize += FruitTypes[typeKey].size;
			fruitTypeCount++;
		}
		avgFruitSize = fruitTypeCount > 0 ? avgFruitSize / fruitTypeCount : 300; // Default if no types
		var cellSize = Math.ceil(avgFruitSize * GAME_CONSTANTS.SPATIAL_GRID_CELL_SIZE_FACTOR);
		spatialGrid = new SpatialGrid(cellSize);
	}
	// Destroy and recreate boundaries and UI elements
	if (wallLeft) {
		wallLeft.destroy();
	}
	if (wallRight) {
		wallRight.destroy();
	}
	if (gameFloor) {
		gameFloor.destroy();
	}
	if (gameOverLine) {
		gameOverLine.destroy();
	}
	if (pineapple) {
		pineapple.destroy();
	} // Special pineapple
	if (trajectoryLine) {
		trajectoryLine.destroy();
	}
	if (evolutionLine) {
		evolutionLine.destroy();
	}
	if (scoreText) {
		// If scoreText was added to LK.gui.top
		LK.gui.top.removeChild(scoreText); // Remove from specific GUI layer
		scoreText.destroy(); // Then destroy the text object itself
		scoreText = null; // Nullify reference
	}
	LK.playMusic('bgmusic'); // Restart music
	setupBoundaries();
	setupUI(); // This will create scoreText and chargedBallUI
	setupPineapple(); // Creates the initial static pineapple
	updateFireBackground(); // Initial fire setup
	trajectoryLine = game.addChild(new TrajectoryLine());
	// trajectoryLine.createDots(); // Called by constructor or as needed by updateTrajectory
	evolutionLine = game.addChild(new EvolutionLine());
	// evolutionLine.initialize(); // Called by its constructor
	updateScoreDisplay(); // Set score to 0 initially
	activeFruit = null; // No fruit being controlled yet
	createNextFruit(); // Prepare the first fruit
	resetChargedBalls(); // Ensure charge UI is reset
}
function spawnCoconut() {
	var coconut = new Fruit(FruitTypes.COCONUT);
	var minX = wallLeft.x + wallLeft.width / 2 + coconut.width / 2 + 30; // Buffer from wall
	var maxX = wallRight.x - wallRight.width / 2 - coconut.width / 2 - 30;
	coconut.x = minX + Math.random() * (maxX - minX);
	coconut.y = gameHeight + coconut.height; // Start below screen
	coconut.isStatic = true; // Initially static for tweening
	game.addChild(coconut);
	// Do not add to fruits array or spatial grid until it becomes dynamic
	coconut.immuneToGameOver = true; // Immune during spawn animation
	var targetY = gameHeight - gameFloor.height / 2 - coconut.height / 2 - 10; // Target just above floor
	tween(coconut, {
		y: targetY
	}, {
		duration: 1200,
		// Tween duration
		easing: tween.easeOut,
		// Smoother easing for arrival
		onFinish: function onFinish() {
			if (!coconut || coconut.parent === null) {
				return;
			} // Check if still exists
			coconut.isStatic = false; // Now it's dynamic
			fruits.push(coconut); // Add to main simulation arrays
			if (spatialGrid) {
				spatialGrid.insertObject(coconut);
			}
			coconut.vy = -1.5; // Small upward pop
			coconut.vx = (Math.random() * 2 - 1) * 1.0; // Small random horizontal push
			LK.setTimeout(function () {
				if (coconut && fruits.indexOf(coconut) !== -1) {
					coconut.immuneToGameOver = false;
				}
			}, 1000); // Immunity wears off
		}
	});
}
// --- Player Input ---
game.down = function (x, y) {
	if (activeFruit && !gameOver && isClickable) {
		// Check isClickable here too for initial press
		isDragging = true;
		game.move(x, y); // Process initial position
	}
};
game.move = function (x, y) {
	if (isDragging && activeFruit && !gameOver) {
		var fruitRadius = activeFruit.width / 2;
		// Constrain X position within walls
		var minX = wallLeft.x + wallLeft.width / 2 + fruitRadius;
		var maxX = wallRight.x - wallRight.width / 2 - fruitRadius;
		activeFruit.x = Math.max(minX, Math.min(maxX, x));
		// Y position is fixed for aiming
		activeFruit.y = dropPointY - GAME_CONSTANTS.DROP_START_Y_OFFSET + 300;
		if (trajectoryLine) {
			trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y + GAME_CONSTANTS.DROP_START_Y_OFFSET * 2);
		}
	}
};
game.up = function () {
	if (isDragging && activeFruit && isClickable && !gameOver) {
		dropFruit();
	}
	isDragging = false; // Reset dragging state regardless
};
// --- Main Game Loop ---
function updatePhysicsMain() {
	// Renamed from updatePhysics to avoid conflict with Fruit.updatePhysics
	// Rebuild spatial grid periodically (e.g., if fruits stop moving but grid needs refresh)
	// More robust would be to rebuild if many fruits are marked dirty or after many removals.
	if (spatialGrid && Date.now() - spatialGrid.lastRebuildTime > spatialGrid.rebuildInterval) {
		spatialGrid.rebuildGrid(fruits);
	}
	// Phase 1: Apply physics forces and update velocities/positions for each fruit
	for (var i = fruits.length - 1; i >= 0; i--) {
		var fruit = fruits[i];
		if (!fruit || fruit.isStatic || fruit.merging) {
			continue;
		}
		fruit.updatePhysics(); // Call the fruit's own physics update method
	}
	// Phase 2: Check and resolve fruit-to-fruit collisions
	checkFruitCollisions(); // Global function for inter-fruit collisions
	// Phase 3: Check and resolve boundary collisions, then finalize positions and grid updates
	for (var i = fruits.length - 1; i >= 0; i--) {
		var fruit = fruits[i];
		if (!fruit || fruit.isStatic || fruit.merging) {
			continue;
		}
		var walls = {
			left: wallLeft,
			right: wallRight
		};
		fruit.checkBoundaries(walls, gameFloor); // Fruit's method for boundary checks
		// Final check for very small velocities if fruit is stabilized or on ground to help it fully stop
		if ((fruit.isFullyStabilized || fruit._boundaryContacts && fruit._boundaryContacts.floor) && !fruit.isSleeping) {
			if (Math.abs(fruit.vx) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD * 0.5) {
				fruit.vx = 0;
			}
			if (Math.abs(fruit.vy) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD * 0.5) {
				fruit.vy = 0;
			}
			if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.STABLE_ANGULAR_VELOCITY_THRESHOLD * 0.5) {
				fruit.angularVelocity = 0;
			}
		}
		// Update fruit's position in the spatial grid after all movements and collision resolutions
		if (spatialGrid) {
			spatialGrid.updateObject(fruit);
		}
	}
	// Phase 4: (Original) Environmental interactions and position adjustments
	// The "gap below" logic was very complex and might be better handled by more robust general physics.
	// For now, this is removed to simplify. If fruits get stuck, simpler nudging can be added.
}
game.update = function () {
	if (gameOver) {
		return; // Stop updates if game over
	}
	// Coconut spawning logic
	var currentScore = LK.getScore();
	if (currentScore >= lastScoreCheckForCoconut + GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) {
		// Update score checkpoint based on actual score and interval, to prevent multiple spawns if score jumps high
		lastScoreCheckForCoconut = Math.floor(currentScore / GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) * GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL;
		spawnCoconut();
	}
	// This else-if ensures that if the score doesn't trigger a spawn, but surpasses the last checkpoint,
	// the checkpoint is updated. This handles cases where score might increase by less than the interval.
	// However, the above line `lastScoreCheckForCoconut = Math.floor(...)` already handles this.
	// else if (currentScore > lastScoreCheckForCoconut) { 
	// 	lastScoreCheckForCoconut = Math.floor(currentScore / GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) * GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL;
	// }
	updatePhysicsMain(); // Main physics and collision update loop
	checkGameOver(); // Check game over conditions
	// Update visual effects like fire
	updateFireBackground();
	for (var i = 0; i < activeFireElements.length; i++) {
		if (activeFireElements[i]) {
			activeFireElements[i].update(); // Assuming FireElement has an update method
		}
	}
};
// Initialize and start the game
initGame();
// --- Fire Background Logic ---
function updateFireBackground() {
	var uniqueFruitTypes = {};
	for (var i = 0; i < fruits.length; i++) {
		var fruit = fruits[i];
		// Consider only dynamic, non-merging fruits with valid types
		if (fruit && !fruit.isStatic && !fruit.merging && fruit.type && fruit.type.id) {
			uniqueFruitTypes[fruit.type.id] = true;
		}
	}
	var uniqueTypeCount = Object.keys(uniqueFruitTypes).length;
	var targetFireCount = GAME_CONSTANTS.FIRE_BASE_COUNT + Math.floor(uniqueTypeCount / GAME_CONSTANTS.FIRE_FRUIT_TYPE_THRESHOLD);
	targetFireCount = Math.min(targetFireCount, GAME_CONSTANTS.FIRE_MAX_COUNT);
	// Add new fire elements if needed
	while (activeFireElements.length < targetFireCount) {
		createFireElement(activeFireElements.length); // Pass current count as index for positioning
	}
	// Remove excess fire elements
	while (activeFireElements.length > targetFireCount) {
		var fireToRemove = activeFireElements.pop();
		if (fireToRemove) {
			fireToRemove.destroy(); // Ensure proper cleanup of the fire element
			// fireContainer.removeChild(fireToRemove); // Destroy should handle this if it's a PIXI object
		}
	}
}
function createFireElement(index) {
	// Calculate position based on index to stack them or spread them out
	var yPos = gameHeight + GAME_CONSTANTS.FIRE_START_Y_OFFSET - index * GAME_CONSTANTS.FIRE_STACK_Y_OFFSET;
	var xPos = gameWidth / 2 + (Math.random() * GAME_CONSTANTS.FIRE_X_SPREAD - GAME_CONSTANTS.FIRE_X_SPREAD / 2);
	var newFire = new FireElement(xPos, yPos); // Assuming FireElement constructor sets up PIXI parts
	if (index % 2 === 1) {
		// Alternate facing direction for visual variety
		if (newFire.fireAsset) {
			newFire.fireAsset.scaleX = -1;
		}
		if (newFire.fireAsset2) {
			newFire.fireAsset2.scaleX = -1;
		}
	}
	fireContainer.addChildAt(newFire, 0); // Add to the bottom of the fire container for layering
	activeFireElements.push(newFire);
}
function removeFruitFromGame(fruit) {
	if (!fruit) {
		return;
	}
	// Wake up any sleeping neighbors that might have been resting on this fruit.
	// This is a simplified approach. More robust would be to check if they *were* supported.
	if (fruit.neighborContacts && fruit.neighborContacts.length > 0) {
		for (var i = 0; i < fruit.neighborContacts.length; i++) {
			var neighborId = fruit.neighborContacts[i];
			for (var j = 0; j < fruits.length; j++) {
				if (fruits[j] && fruits[j].id === neighborId) {
					if (fruits[j].isSleeping || fruits[j].isFullyStabilized) {
						fruits[j].wakeUp(); // Use the fruit's own wakeUp method
						// Optionally give a very small nudge to ensure physics re-evaluates
						fruits[j].vy -= 0.05; // Tiny upward nudge
						fruits[j].vx += (Math.random() - 0.5) * 0.1; // Tiny sideways nudge
					}
					break;
				}
			}
		}
	}
	var index = fruits.indexOf(fruit);
	if (index !== -1) {
		fruits.splice(index, 1);
	}
	if (spatialGrid) {
		spatialGrid.removeObject(fruit);
	}
	// If the fruit being removed was the 'lastDroppedFruit', nullify the reference
	if (lastDroppedFruit === fruit) {
		lastDroppedFruit = null;
	}
	// If it was the active aiming fruit (shouldn't happen if physics are involved, but defensive)
	if (activeFruit === fruit) {
		activeFruit = null;
		// Potentially createNextFruit() here if game logic requires immediate replacement
	}
	fruit.destroy(); // Call the fruit's own destroy method (should handle PIXI cleanup)
} ===================================================================
--- original.js
+++ change.js
@@ -955,9 +955,13 @@
 	self.activeDots = []; // DisplayObjects currently on stage
 	// self.dots renamed to activeDots for clarity
 	self.dotSpacing = 25; // Spacing between dots
 	self.dotSize = 15; // Visual size (not used in current createObject)
-	self.maxDots = 35; // Max dots to show for trajectory (reduced for performance)
+	// Dynamically calculate maxDots so the trajectory can reach from drop point to floor
+	var dropActualY = GAME_CONSTANTS.DROP_POINT_Y + GAME_CONSTANTS.DROP_START_Y_OFFSET;
+	var distanceToCover = GAME_CONSTANTS.GAME_HEIGHT - dropActualY;
+	self.maxDots = Math.ceil(distanceToCover / self.dotSpacing) + 5; // +5 for a little buffer
+	if (self.maxDots <= 0) self.maxDots = 60; // Fallback if calculation is off
 	self.createDots = function () {
 		// Renamed from initialize for clarity
 		self.clearDots(); // Clear existing before creating new pool (if any)
 		// DotPool's initialize is called by its constructor if initialSize is given
@@ -972,86 +976,79 @@
 		}
 	};
 	self.updateTrajectory = function (startX, startY) {
 		if (!activeFruit) {
+			// Assumes activeFruit is a global variable for the fruit being aimed
 			self.clearDots(); // Clear if no active fruit
 			return;
 		}
 		self.clearDots(); // Clear previous trajectory
-		// Start the first dot just below the fruit (bottom edge)
-		var currentFruitRadius = activeFruit.width / 2;
-		var dotSpacing = self.dotSpacing;
+		var currentY = startY;
 		var dotCount = 0;
 		var hitDetected = false;
-		// Calculate the maximum Y the dot can go (floor)
-		var floorCollisionY = gameFloor.y - gameFloor.height / 2 - currentFruitRadius;
-		// Start just below the fruit
-		var currentY = startY + currentFruitRadius + 2; // 2px buffer to avoid overlap
-		// Always use the current X of the fruit for the trajectory
-		var currentX = startX;
-		// Defensive: avoid infinite loop
-		var maxDots = self.maxDots;
-		while (dotCount < maxDots && !hitDetected) {
+		var currentFruitRadius = activeFruit.width / 2; // Use current active fruit's size
+		// Reduce the number of fine-grained collision checks for trajectory dots
+		var COLLISION_CHECK_INTERVAL = 3; // Only check every 3rd dot for collisions
+		while (dotCount < self.maxDots && !hitDetected) {
 			var dot = self.dotPool.get();
 			if (!dot) {
 				continue;
-			}
+			} // Should not happen if pool can grow or is pre-filled
 			self.addChild(dot);
 			self.activeDots.push(dot);
-			dot.x = currentX;
+			dot.x = startX;
 			dot.y = currentY;
 			dot.visible = true;
-			dot.alpha = 1.0 - dotCount / maxDots * 0.7;
-			dot.scale.set(1.0 - dotCount / maxDots * 0.5);
+			dot.alpha = 1.0 - dotCount / self.maxDots * 0.7; // Fade out dots further down
+			dot.scale.set(1.0 - dotCount / self.maxDots * 0.5); // Make dots smaller further down
 			// Check for floor collision
-			if (currentY >= floorCollisionY) {
-				dot.y = floorCollisionY;
+			var floorCollisionY = gameFloor.y - gameFloor.height / 2 - currentFruitRadius; // Adjusted for fruit radius
+			if (currentY > floorCollisionY) {
+				if (self.activeDots.length > 0) {
+					// Adjust last dot to touch floor
+					self.activeDots[self.activeDots.length - 1].y = floorCollisionY;
+				}
 				hitDetected = true;
-				break;
+				break; // Stop trajectory at floor
 			}
-			// Check for fruit collisions every dot
-			var trajectoryCheckObject = {
-				x: currentX,
-				y: currentY,
-				width: activeFruit.width,
-				height: activeFruit.height,
-				id: 'trajectory_check_point'
-			};
-			var potentialHits = spatialGrid.getPotentialCollisions(trajectoryCheckObject);
-			for (var j = 0; j < potentialHits.length; j++) {
-				var fruit = potentialHits[j];
-				if (fruit && fruit !== activeFruit && !fruit.merging && !fruit.isStatic && fruit.width && fruit.height) {
-					// Use circular collision for more natural intersection
-					var dx = currentX - fruit.x;
-					var dy = currentY - fruit.y;
-					var rSum = currentFruitRadius + fruit.width / 2;
-					var distSq = dx * dx + dy * dy;
-					if (distSq < rSum * rSum) {
-						// Place the last dot just before the collision
-						if (self.activeDots.length > 0) {
-							var lastDot = self.activeDots[self.activeDots.length - 1];
-							var dist = Math.sqrt(distSq);
-							if (dist > 0) {
-								// Move it to the edge of the fruit, minus a small buffer
-								var adjust = (dist - rSum + 2) / dist;
-								lastDot.x = fruit.x + dx * adjust;
-								lastDot.y = fruit.y + dy * adjust;
+			// Only check for fruit collisions every COLLISION_CHECK_INTERVAL dots
+			if (dotCount % COLLISION_CHECK_INTERVAL === 0) {
+				// Create a temporary bounding box for the current trajectory point
+				var trajectoryCheckObject = {
+					x: startX,
+					y: currentY,
+					width: activeFruit.width,
+					// Use active fruit's dimensions for check
+					height: activeFruit.height,
+					id: 'trajectory_check_point' // Unique ID for spatial grid
+				};
+				var potentialHits = spatialGrid.getPotentialCollisions(trajectoryCheckObject);
+				for (var j = 0; j < potentialHits.length; j++) {
+					var fruit = potentialHits[j];
+					// Ensure fruit is valid and not the active fruit itself, and not merging/static
+					if (fruit && fruit !== activeFruit && !fruit.merging && !fruit.isStatic && fruit.width && fruit.height) {
+						// Simple AABB intersection check for trajectory prediction
+						if (self.wouldIntersectFruit(startX, currentY, activeFruit, fruit)) {
+							if (self.activeDots.length > 0) {
+								// Adjust the last dot's position to be just before the collision
+								// This is a simplification; true surface point is harder.
+								// We can estimate by moving it back slightly along the drop path.
+								var lastDot = self.activeDots[self.activeDots.length - 1];
+								lastDot.y = fruit.y - fruit.height / 2 - activeFruit.height / 2 - 2; // Place above hit fruit
 								if (lastDot.y > floorCollisionY) {
 									lastDot.y = floorCollisionY;
-								}
-							} else {
-								lastDot.y = fruit.y - rSum;
+								} // Don't go below floor
 							}
+							hitDetected = true;
+							break;
 						}
-						hitDetected = true;
-						break;
 					}
 				}
+				if (hitDetected) {
+					break;
+				}
 			}
-			if (hitDetected) {
-				break;
-			}
-			currentY += dotSpacing;
+			currentY += self.dotSpacing;
 			dotCount++;
 		}
 	};
 	// Simplified intersection check for trajectory (AABB based)
@@ -1071,9 +1068,9 @@
 /**** 
 * Initialize Game
 ****/ 
 var game = new LK.Game({
-	backgroundColor: 0x000000
+	backgroundColor: 0xffe122
 });
 
 /**** 
 * Game Code
@@ -1461,9 +1458,8 @@
 	activeFruit.isStatic = true; // Static until dropped
 	game.addChild(activeFruit);
 	if (trajectoryLine) {
 		trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y + GAME_CONSTANTS.DROP_START_Y_OFFSET * 2); // Start trajectory from actual drop height
-		trajectoryLine.visible = true;
 	}
 }
 function dropFruit() {
 	if (gameOver || !activeFruit || !isClickable) {
@@ -1498,10 +1494,10 @@
 			activeFruit.mergeGracePeriodActive = false;
 		}
 	}, GAME_CONSTANTS.MERGE_GRACE_MS);
 	if (trajectoryLine) {
-		// Keep trajectory line visible for the next fruit (do not clear here)
-		trajectoryLine.visible = false; // Hide while fruit is falling
+		// Clear trajectory line after dropping
+		trajectoryLine.clearDots();
 	}
 	// Reset 'fromChargedRelease' flag for all existing fruits (if it was used)
 	for (var i = 0; i < fruits.length; i++) {
 		if (fruits[i] && fruits[i].fromChargedRelease) {
@@ -1976,21 +1972,18 @@
 		game.move(x, y); // Process initial position
 	}
 };
 game.move = function (x, y) {
-	if (activeFruit && !gameOver) {
+	if (isDragging && activeFruit && !gameOver) {
 		var fruitRadius = activeFruit.width / 2;
 		// Constrain X position within walls
 		var minX = wallLeft.x + wallLeft.width / 2 + fruitRadius;
 		var maxX = wallRight.x - wallRight.width / 2 - fruitRadius;
-		if (isDragging) {
-			activeFruit.x = Math.max(minX, Math.min(maxX, x));
-		}
+		activeFruit.x = Math.max(minX, Math.min(maxX, x));
 		// Y position is fixed for aiming
 		activeFruit.y = dropPointY - GAME_CONSTANTS.DROP_START_Y_OFFSET + 300;
 		if (trajectoryLine) {
 			trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y + GAME_CONSTANTS.DROP_START_Y_OFFSET * 2);
-			trajectoryLine.visible = true;
 		}
 	}
 };
 game.up = function () {