User prompt
make the balls drop even faster
User prompt
balls can still rotate exponentially. ground applies friction, but collision with other balls doesnt, making balls on top spin uncontrolably. fix this please
User prompt
the rotation seems to be too much and looks unrealistic. ensure ground adds friction which decreases the rotation
User prompt
balls should also rotate when colliding with other balls. the rotation should follow physics rules, based on the point of impact ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
remove the numerical value from the balls
User prompt
balls numbers are wrng, smallest fruit should be 1, next one 2 instead of 3, next one 3 instead of 6 and so on, keep it simple, chronological
User prompt
the smallest starting fruit size should be 100 and each next evolution is 100 pixels higher, so first is 100, next 200, next 300 and so on. change not the values in the code, but also the actual assets sizes
User prompt
the smallest starting fruit size should be 150 and each next evolution is 150 pixels higher, so first is 150, next 300, next 450 and so on
User prompt
the game seems to only have 5 fruits, I need another 5, so we have 10 total
User prompt
there seems to b a lag before identical balls merge, the merge should hapen instantly, as soon as the balls touch
User prompt
only balls of different evolutions should colide, identical ones need to merge
User prompt
balls currently overlap and intersect each other, that should not be possible, they need to collide with each other, and never overlap
User prompt
upon merging, balls should evolve to the next level, right now they dont, they just disappear. that's ok, but also create a new higher level ball upon merging
User prompt
increase the balls drop speed
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'length')' in or related to this line: 'var fruitGraphics = self.attachAsset(self.type.id, {' Line Number: 39
User prompt
the game goes to game over randomly after releasing balls, fix this bug
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'length')' in or related to this line: 'var fruitGraphics = self.attachAsset(self.type.id, {' Line Number: 39
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'length')' in or related to this line: 'var fruitGraphics = self.attachAsset(self.type.id, {' Line Number: 36
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'setText')' in or related to this line: 'scoreText.setText("SCORE: " + LK.getScore());' Line Number: 303
Code edit (1 edits merged)
Please save this source code
User prompt
Fruit Fusion: Bouncy Merge Mania
Initial prompt
Fruit merge - physics based game with bouncing balls
/**** * 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 = 15; self.currentCharge = 0; self.isReadyToRelease = false; self.countdownText = null; self.pulseAnimationActive = false; self.initialize = function () { self.portalAsset = self.attachAsset('portal', { anchorX: 0.5, anchorY: 0.5 }); self.portalAsset.x = gameWidth / 2 + 870; self.portalAsset.alpha = 0; // Start invisible self.portalAsset.scaleX = 0; self.portalAsset.scaleY = 0; self.y = 120; }; self.updateChargeDisplay = function (chargeCount) { self.currentCharge = chargeCount; var remainingCount = Math.max(0, self.chargeNeededForRelease - self.currentCharge); // No hue/tint changes for portal - keep original appearance // Calculate size based on charge progress var progressPercent = (self.chargeNeededForRelease - remainingCount) / self.chargeNeededForRelease; var targetScale = progressPercent; // Make portal visible once charging starts if (progressPercent > 0 && self.portalAsset.alpha === 0) { self.portalAsset.alpha = 1; } tween(self.portalAsset, { scaleX: targetScale, scaleY: targetScale, alpha: progressPercent // Gradually increase visibility }, { duration: 300, 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 // Make portal spin clockwise 360 degrees }, { duration: 300, 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: 500, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.isReadyToRelease) { self.pulseAnimationActive = false; return; } tween(self.portalAsset, { scaleX: 1.0, scaleY: 1.0 }, { duration: 500, 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 }); }; return self; }); var CollisionComponent = Container.expand(function () { var self = Container.call(this); 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; var fruitLevel = getFruitLevel(fruit); var sizeReduction = 0; if (fruitLevel > 5) { sizeReduction = (fruitLevel - 5) * 3; } fruitHalfWidth = Math.max(10, fruitHalfWidth - sizeReduction / 2); fruitHalfHeight = Math.max(10, fruitHalfHeight - sizeReduction / 2); 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; var wasInContact = fruit.wallContactFrames > 0; fruit._boundaryContacts = fruit._boundaryContacts || { left: false, right: false, floor: false }; 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 isInContact = fruit._boundaryContacts.left || fruit._boundaryContacts.right || fruit._boundaryContacts.floor; if (isInContact) { fruit.wallContactFrames++; var progressiveFriction = Math.min(0.85, 0.65 + fruit.wallContactFrames * 0.01); fruit.angularVelocity *= progressiveFriction; if (fruit.wallContactFrames > 10) { fruit.vx *= 0.9; fruit.vy *= 0.9; } } else { fruit.wallContactFrames = Math.max(0, fruit.wallContactFrames - 1); } }; self.checkLeftWallCollision = function (fruit, leftWall, effectiveWidth) { var leftBoundary = leftWall.x + leftWall.width / 2 + effectiveWidth; if (fruit.x < leftBoundary) { fruit.x = leftBoundary; fruit.vx = -fruit.vx * fruit.elasticity * 0.7; var angularImpactMultiplier = 0.0025 * (1 + (0.9 - fruit.elasticity) * 5); fruit.angularVelocity += fruit.vy * angularImpactMultiplier * 0.25; fruit.angularVelocity *= fruit.groundAngularFriction * 0.8; fruit._boundaryContacts.left = true; if (fruit.wallContactFrames > 3 && Math.abs(fruit.vx) < 0.8) { fruit.vx = 0; } } else if (fruit.x > leftWall.x + leftWall.width * 2) { if (fruit._boundaryContacts && !fruit._boundaryContacts.right && !fruit._boundaryContacts.floor) { fruit.wallContactFrames = Math.max(0, fruit.wallContactFrames - 1); } } }; self.checkRightWallCollision = function (fruit, rightWall, effectiveWidth) { var rightBoundary = rightWall.x - rightWall.width / 2 - effectiveWidth; if (fruit.x > rightBoundary) { fruit.x = rightBoundary; fruit.vx = -fruit.vx * fruit.elasticity * 0.7; var angularImpactMultiplier = 0.0025 * (1 + (0.9 - fruit.elasticity) * 5); fruit.angularVelocity -= fruit.vy * angularImpactMultiplier * 0.25; fruit.angularVelocity *= fruit.groundAngularFriction * 0.8; fruit._boundaryContacts.right = true; if (fruit.wallContactFrames > 3 && Math.abs(fruit.vx) < 0.8) { fruit.vx = 0; } } }; self.checkFloorCollision = function (fruit, floor, effectiveHeight) { var floorCollisionY = floor.y - floor.height / 2 - effectiveHeight; if (fruit.y > floorCollisionY) { fruit.y = floorCollisionY; var oldVy = fruit.vy; fruit.vy = -fruit.vy * fruit.elasticity * 0.5; fruit._boundaryContacts.floor = true; if (Math.abs(fruit.vx) > 0.5) { fruit.angularVelocity = fruit.vx * 0.01; } fruit.angularVelocity *= fruit.groundAngularFriction * 0.8; var restThreshold = fruit.wallContactFrames > 5 ? 1.5 : 2.5; if (Math.abs(fruit.vy) < restThreshold) { fruit.vy = 0; fruit.vx *= 0.7; if (fruit.wallContactFrames > 8) { fruit.vx *= 0.85; } } var angularRestThreshold = fruit.wallContactFrames > 5 ? 0.015 : 0.025; if (Math.abs(fruit.angularVelocity) < angularRestThreshold) { 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 = 120; self.x = gameWidth / 2 + 350; // Moved 300 pixels to the right 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(150 / fruitIcon.width, 150 / fruitIcon.height); fruitIcon.scaleX = scale; fruitIcon.scaleY = scale; totalWidth += fruitIcon.width * scale; if (i < fruitTypes.length - 1) { totalWidth += 100; // Increased 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 + 20; // Increased spacing } }; return self; }); var FireElement = Container.expand(function (initX, initY, zIndex) { var self = Container.call(this); // Configuration 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; // Alpha range changed to 0.1-0.5 self.alphaMin = 0.1; self.alphaMax = 0.5; self.flickerSpeed = 500 + Math.random() * 300; self.frameIndex = 0; self.frameTimer = null; self.frameDuration = 200; // Switch frames every 300ms // Setup self.initialize = function () { // Create both fire frames (one visible, one hidden initially) self.fireAsset = self.attachAsset('fire', { anchorX: 0.5, anchorY: 1.0 // Anchor at bottom-center }); self.fireAsset2 = self.attachAsset('fire_2', { anchorX: 0.5, anchorY: 1.0 // Anchor at bottom-center }); self.fireAsset2.visible = false; // Start with frame 2 hidden self.x = self.baseX; self.y = self.baseY; // Start animations self.startAlphaFlicker(); self.startFrameAnimation(); }; // Horizontal sway animation self.update = function () { // Update horizontal position with sway animation self.x += self.movementSpeed * self.direction; // Check if we need to change direction if (Math.abs(self.x - self.baseX) > self.movementRange) { self.direction *= -1; } }; // Frame animation self.startFrameAnimation = function () { // Clear any existing frame timer if (self.frameTimer) { LK.clearInterval(self.frameTimer); } // Create new interval for frame switching self.frameTimer = LK.setInterval(function () { self.toggleFrame(); }, self.frameDuration); }; self.toggleFrame = function () { // Switch between frames self.frameIndex = (self.frameIndex + 1) % 2; self.fireAsset.visible = self.frameIndex === 0; self.fireAsset2.visible = self.frameIndex === 1; }; // Alpha flicker animation self.startAlphaFlicker = function () { if (self.flickerTween) { self.flickerTween.stop(); } // Randomize starting delay to avoid synchronized flickers var startDelay = Math.random() * 500; LK.setTimeout(function () { self.flickerToMax(); }, startDelay); }; self.flickerToMax = function () { // Apply alpha flicker to both frames self.flickerTween = tween(self, { alpha: self.alphaMax // Use class variable instead of hardcoded value }, { duration: self.flickerSpeed, easing: tween.easeInOut, onFinish: self.flickerToMin }); }; self.flickerToMin = function () { // Apply alpha flicker to both frames self.flickerTween = tween(self, { alpha: self.alphaMin // Use class variable instead of hardcoded value }, { 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); }; 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; var physics = new PhysicsComponent(); var collision = new CollisionComponent(); var mergeHandler = new MergeComponent(); var behaviorSystem = new FruitBehavior(); self.vx = physics.vx; self.vy = physics.vy; self.rotation = physics.rotation; self.angularVelocity = physics.angularVelocity; self.angularFriction = physics.angularFriction; self.groundAngularFriction = physics.groundAngularFriction; var currentLevel = getFruitLevel(self); self.gravity = physics.gravity * (1 + (currentLevel - 1) * 0.15); self.friction = physics.friction; self.rotationRestCounter = physics.rotationRestCounter; self.maxAngularVelocity = physics.maxAngularVelocity; self.isStatic = physics.isStatic; self.elasticity = currentLevel <= 3 ? 0.85 : 0.8 - (currentLevel - 1) * (0.2 / 9); self.wallContactFrames = collision.wallContactFrames; self.merging = mergeHandler.merging; self.mergeGracePeriodActive = mergeHandler.mergeGracePeriodActive; self.fromChargedRelease = mergeHandler.fromChargedRelease; self.safetyPeriod = false; self.immuneToGameOver = false; 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.log("Warning: Fruit type not available yet or missing required properties"); } self.updatePhysics = function () { physics.apply(self); }; self.checkBoundaries = function (walls, floor) { collision.checkBoundaryCollisions(self, walls, floor); if (self.safetyPeriod === false && self.vy <= 0.1) { self.safetyPeriod = undefined; } }; self.merge = function (otherFruit) { mergeHandler.beginMerge(self, otherFruit); }; return self; }); var FruitBehavior = Container.expand(function () { var self = Container.call(this); self.behaviors = { CHERRY: { onMerge: function onMerge(fruit1, fruit2, posX, posY) { return self.standardMerge(fruit1, fruit2, posX, posY); } }, GRAPE: { onMerge: function onMerge(fruit1, fruit2, posX, posY) { return self.standardMerge(fruit1, fruit2, posX, posY); } }, APPLE: { onMerge: function onMerge(fruit1, fruit2, posX, posY) { return self.standardMerge(fruit1, fruit2, posX, posY); } }, ORANGE: { onMerge: function onMerge(fruit1, fruit2, posX, posY) { return self.standardMerge(fruit1, fruit2, posX, posY); } }, WATERMELON: { onMerge: function onMerge(fruit1, fruit2, posX, posY) { return self.standardMerge(fruit1, fruit2, posX, posY); } }, PINEAPPLE: { onMerge: function onMerge(fruit1, fruit2, posX, posY) { return self.standardMerge(fruit1, fruit2, posX, posY); }, onSpawn: function onSpawn() {} }, MELON: { onMerge: function onMerge(fruit1, fruit2, posX, posY) { LK.getSound('Smartz').play(); return self.standardMerge(fruit1, fruit2, posX, posY); } }, PEACH: { onMerge: function onMerge(fruit1, fruit2, posX, posY) { LK.getSound('stonks').play(); return self.standardMerge(fruit1, fruit2, posX, posY); } }, COCONUT: { onMerge: function onMerge(fruit1, fruit2, posX, posY) { LK.getSound('ThisIsFine').play(); return self.standardMerge(fruit1, fruit2, posX, posY); }, onSpawn: function onSpawn() { LK.getSound('stonks').play(); } }, DURIAN: { onMerge: function onMerge(fruit1, fruit2, posX, posY) { LK.setScore(LK.getScore() + fruit1.type.points); updateScoreDisplay(); removeFruitFromGame(fruit1); removeFruitFromGame(fruit2); releasePineappleOnMerge(); return null; } } }; self.getMergeHandler = function (fruitTypeId) { if (!fruitTypeId) { return self.behaviors.CHERRY; } var upperTypeId = fruitTypeId.toUpperCase(); return self.behaviors[upperTypeId] || self.behaviors.CHERRY; }; self.standardMerge = function (fruit1, fruit2, posX, posY) { var nextType = FruitTypes[fruit1.type.next.toUpperCase()]; releasePineappleOnMerge(); return self.createNextLevelFruit(fruit1, nextType, posX, posY); }; self.createNextLevelFruit = function (sourceFruit, nextType, posX, posY) { var newFruit = new Fruit(nextType); newFruit.x = posX; newFruit.y = posY; newFruit.scaleX = 0.5; newFruit.scaleY = 0.5; game.addChild(newFruit); fruits.push(newFruit); spatialGrid.insertObject(newFruit); LK.setScore(LK.getScore() + nextType.points); updateScoreDisplay(); tween(newFruit, { scaleX: 1, scaleY: 1 }, { duration: 300, 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', { anchorX: 0.5, anchorY: 0.5 }); lineGraphics.tint = 0xff0000; lineGraphics.height = 20; return self; }); var MergeComponent = Container.expand(function () { var self = Container.call(this); self.merging = false; self.mergeGracePeriodActive = false; self.fromChargedRelease = false; self.fruitBehavior = new FruitBehavior(); self.beginMerge = function (fruit1, fruit2) { if (fruit1.merging) { 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) { tween(fruit1, { alpha: 0, scaleX: 0.5, scaleY: 0.5 }, { duration: 200, easing: tween.easeOut }); tween(fruit2, { alpha: 0, scaleX: 0.5, scaleY: 0.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { self.completeMerge(fruit1, fruit2, midX, midY); } }); }; self.completeMerge = function (fruit1, fruit2, midX, midY) { LK.getSound('merge').play(); self.trackMerge(fruit1, fruit2); var behaviorHandler = self.fruitBehavior.getMergeHandler(fruit1.type.id); behaviorHandler.onMerge(fruit1, fruit2, midX, midY); if (fruit1 && fruits.includes(fruit1) && fruit1.type.id.toUpperCase() !== 'DURIAN') { removeFruitFromGame(fruit1); } if (fruit2 && fruits.includes(fruit2)) { removeFruitFromGame(fruit2); } }; self.trackMerge = function (fruit1, fruit2) { 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); self.vx = 0; self.vy = 0; self.gravity = 5.0; self.friction = 0.92; self.elasticity = 0.4; self.isStatic = false; self.rotation = 0; self.angularVelocity = 0; self.angularFriction = 0.85; self.groundAngularFriction = 0.6; self.maxAngularVelocity = 0.08; self.rotationRestCounter = 0; self.lastVx = 0; self.lastVy = 0; self.stabilizeFruit = function (fruit, movementMagnitude, velocityChange) { var shouldStabilize = false; var stabilizationLevel = 0; var fruitLevel = getFruitLevel(fruit); if (!fruit._stabilizeMetrics) { fruit._stabilizeMetrics = { consecutiveSmallMovements: 0, wallContactDuration: 0, surroundedDuration: 0, restingDuration: 0 }; } var massFactor = Math.pow(fruitLevel, 1.5) / 10; var movementThreshold = 0.4 - (fruitLevel - 1) * 0.03; var angularThreshold = 0.04 - (fruitLevel - 1) * 0.003; if (movementMagnitude < movementThreshold && Math.abs(fruit.angularVelocity) < angularThreshold) { fruit._stabilizeMetrics.consecutiveSmallMovements += 1 + massFactor * 0.2; fruit._stabilizeMetrics.restingDuration += 1 + massFactor * 0.2; } else { fruit._stabilizeMetrics.consecutiveSmallMovements = 0; fruit._stabilizeMetrics.restingDuration = Math.max(0, fruit._stabilizeMetrics.restingDuration - 2); } if (fruit.wallContactFrames > 0) { fruit._stabilizeMetrics.wallContactDuration += 1 + massFactor * 0.15; } else { fruit._stabilizeMetrics.wallContactDuration = Math.max(0, fruit._stabilizeMetrics.wallContactDuration - 1); } if (fruit.surroundedFrames > 0) { fruit._stabilizeMetrics.surroundedDuration += 1 + massFactor * 0.25; } else { fruit._stabilizeMetrics.surroundedDuration = Math.max(0, fruit._stabilizeMetrics.surroundedDuration - 1); } var levelFactor = (fruitLevel - 1) * 0.12; var totalStabilizationScore = fruit._stabilizeMetrics.consecutiveSmallMovements * (1.0 + levelFactor) + fruit._stabilizeMetrics.wallContactDuration * (0.8 + levelFactor) + fruit._stabilizeMetrics.surroundedDuration * (1.2 + levelFactor) + fruit._stabilizeMetrics.restingDuration * (0.5 + levelFactor); var fullStabilizationThreshold = 15 - (fruitLevel - 1) * 1.0; var partialStabilizationThreshold = 8 - (fruitLevel - 1) * 0.6; if (totalStabilizationScore > fullStabilizationThreshold) { stabilizationLevel = 2; } else if (totalStabilizationScore > partialStabilizationThreshold) { stabilizationLevel = 1; } if (stabilizationLevel > 0) { var baseDampFactor = stabilizationLevel === 2 ? 0.6 : 0.85; var massDampFactor = baseDampFactor - massFactor * 0.02; var dampFactor = Math.max(0.4, massDampFactor); fruit.vx *= dampFactor; fruit.vy *= dampFactor; fruit.angularVelocity *= dampFactor; var stopThreshold = 0.1 - (fruitLevel - 1) * 0.005; if (stabilizationLevel === 2 && movementMagnitude < stopThreshold) { fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; return true; } } return false; }; self.apply = function (fruit) { if (fruit.isStatic || fruit.merging) { return; } fruit.lastVx = fruit.vx; fruit.lastVy = fruit.vy; var fruitLevel = getFruitLevel(fruit); var gravityMultiplier = 1 + (fruitLevel - 1) * 0.25; fruit.vy += fruit.gravity * gravityMultiplier * 5; var fruitLevel = getFruitLevel(fruit); var maxVelocity = 60 - Math.min(8, (fruitLevel - 1) * 0.8); if (Math.abs(fruit.vx) > maxVelocity) { fruit.vx = fruit.vx > 0 ? maxVelocity : -maxVelocity; } if (Math.abs(fruit.vy) > maxVelocity) { fruit.vy = fruit.vy > 0 ? maxVelocity : -maxVelocity; } if (fruitLevel > 3) { var bottomTendencyForce = (fruitLevel - 3) * 0.018; fruit.vy += bottomTendencyForce; } fruit.x += fruit.vx; fruit.y += fruit.vy; var movementMagnitude = Math.sqrt(fruit.vx * fruit.vx + fruit.vy * fruit.vy); var velocityChange = Math.abs(fruit.vx - fruit.lastVx) + Math.abs(fruit.vy - fruit.lastVy); var isFullyStabilized = self.stabilizeFruit(fruit, movementMagnitude, velocityChange); if (!isFullyStabilized) { if (movementMagnitude > 0.5 || velocityChange > 0.3) { var rotationFactor = 0.015 / (1 + (fruitLevel - 1) * 0.12); var targetAngularVelocity = fruit.vx * rotationFactor; fruit.angularVelocity = fruit.angularVelocity * 0.8 + targetAngularVelocity * 0.2; } else { fruit.angularVelocity *= 0.8; } fruit.rotation += fruit.angularVelocity; var frictionModifier = 0.99 + (fruitLevel - 1) * 0.003; fruit.vx *= fruit.friction * frictionModifier; fruit.vy *= fruit.friction * frictionModifier * 0.99; var horizontalStopThreshold = Math.max(0.05, 0.1 - (fruitLevel - 1) * 0.005); var verticalStopThreshold = Math.max(0.05, 0.1 - (fruitLevel - 1) * 0.005); if (Math.abs(fruit.vx) < horizontalStopThreshold) { fruit.vx = 0; } if (Math.abs(fruit.vy) < verticalStopThreshold && fruit.y > gameHeight - 300) { fruit.vy = 0; } self.handleRotationDamping(fruit); } }; self.handleRotationDamping = function (fruit) { var movementMagnitude = Math.sqrt(fruit.vx * fruit.vx + fruit.vy * fruit.vy); var rotationDampFactor = 0.9; if (movementMagnitude < 0.05) { rotationDampFactor = 0.6; } else if (movementMagnitude < 0.3) { rotationDampFactor = 0.7; } else if (movementMagnitude < 0.5) { rotationDampFactor = 0.8; } else if (movementMagnitude < 0.8) { rotationDampFactor = 0.85; } fruit.angularVelocity *= rotationDampFactor; if (movementMagnitude > 0.3) { var targetDirection = fruit.vx > 0 ? 1 : -1; var currentDirection = fruit.angularVelocity > 0 ? 1 : -1; if (targetDirection !== currentDirection && Math.abs(fruit.vx) > 1.0) { fruit.angularVelocity *= 0.7; } } if (Math.abs(fruit.angularVelocity) < 0.008) { fruit.rotationRestCounter++; if (fruit.rotationRestCounter > 6) { fruit.angularVelocity = 0; } } else { fruit.rotationRestCounter = 0; } fruit.angularVelocity = Math.min(Math.max(fruit.angularVelocity, -fruit.maxAngularVelocity * 1.2), fruit.maxAngularVelocity * 1.2); }; return self; }); var SpatialGrid = Container.expand(function (cellSize) { var self = Container.call(this); self.cellSize = cellSize || 200; self.grid = {}; self.lastRebuildTime = Date.now(); self.rebuildInterval = 60000; self.insertObject = function (obj) { if (!obj || !obj.x || !obj.y || !obj.width || !obj.height || obj.merging || obj.isStatic) { return; } obj._currentCells = obj._currentCells || []; var cells = self.getCellsForObject(obj); obj._currentCells = cells.slice(); for (var i = 0; i < cells.length; i++) { var cellKey = cells[i]; if (!self.grid[cellKey]) { self.grid[cellKey] = []; } if (self.grid[cellKey].indexOf(obj) === -1) { self.grid[cellKey].push(obj); } } }; self.removeObject = function (obj) { if (!obj || !obj.x || !obj.y || !obj.width || !obj.height) { return; } var cells = obj._currentCells || self.getCellsForObject(obj); 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); } if (self.grid[cellKey].length === 0) { delete self.grid[cellKey]; } } } obj._currentCells = []; }; self.getCellsForObject = function (obj) { if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number') { return []; } var cells = []; 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); } } return cells; }; self.updateObject = function (obj) { if (!obj || !obj.x || !obj.y || !obj.width || !obj.height) { return; } var newCells = self.getCellsForObject(obj); var oldCells = obj._currentCells || []; var cellsChanged = false; if (oldCells.length !== newCells.length) { cellsChanged = true; } else { for (var i = 0; i < newCells.length; i++) { if (oldCells.indexOf(newCells[i]) === -1) { cellsChanged = true; break; } } } if (cellsChanged) { self.removeObject(obj); self.insertObject(obj); } }; self.getPotentialCollisions = function (obj) { var candidates = []; var cells = self.getCellsForObject(obj); var addedObjects = {}; 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]; if (otherObj && otherObj !== obj && !addedObjects[otherObj.id]) { candidates.push(otherObj); addedObjects[otherObj.id] = true; } } } } return candidates; }; self.clear = function () { self.grid = {}; self.lastRebuildTime = Date.now(); }; self.rebuildGrid = function (allObjects) { self.grid = {}; self.lastRebuildTime = Date.now(); if (Array.isArray(allObjects)) { for (var i = 0; i < allObjects.length; i++) { 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(100); self.activeDots = []; self.dots = []; self.dotSpacing = 10; self.dotSize = 15; self.maxDots = 100; self.createDots = function () { self.clearDots(); self.dotPool.initialize(self.maxDots); }; self.clearDots = function () { for (var i = 0; i < self.activeDots.length; i++) { if (self.activeDots[i]) { self.removeChild(self.activeDots[i]); self.dotPool.release(self.activeDots[i]); } } self.activeDots = []; }; self.updateTrajectory = function (startX, startY) { if (!activeFruit) { return; } self.clearDots(); var dotY = startY; var dotSpacing = 25; var dotCount = 0; var hitDetected = false; while (dotCount < self.maxDots && !hitDetected) { var dot = self.dotPool.get(); self.addChild(dot); self.activeDots.push(dot); dot.x = startX; dot.y = dotY; dot.visible = true; dot.alpha = 1.0; dotCount++; dotY += dotSpacing; var floorCollisionY = gameFloor.y - gameFloor.height / 2 - activeFruit.height / 2; if (dotY > floorCollisionY) { if (dotCount > 0) { self.activeDots[dotCount - 1].y = floorCollisionY; } hitDetected = true; break; } var potentialHits = spatialGrid.getPotentialCollisions({ x: startX, y: dotY, width: activeFruit.width, height: activeFruit.height, id: 'trajectory_check' }); for (var j = 0; j < potentialHits.length; j++) { var fruit = potentialHits[j]; if (fruit && fruit !== activeFruit && !fruit.merging && fruit.width && fruit.height) { if (self.wouldIntersectFruit(fruit.x, fruit.y, startX, dotY, activeFruit, fruit)) { if (dotCount > 0) { var dx = fruit.x - startX; var dy = fruit.y - dotY; var dist = Math.sqrt(dx * dx + dy * dy); var overlap = activeFruit.width / 2 + fruit.width / 2 - dist; if (dist > 0) { self.activeDots[dotCount - 1].y = dotY - dy / dist * overlap; } } hitDetected = true; break; } } } } }; self.wouldIntersectFruit = function (fruitX, fruitY, dropX, dropY, activeFruitObj, targetFruitObj) { var dx = fruitX - dropX; var dy = fruitY - dropY; var distanceSquared = dx * dx + dy * dy; var activeFruitLevel = getFruitLevel(activeFruitObj); var targetFruitLevel = getFruitLevel(targetFruitObj); var activeFruitRadius = activeFruitObj.width / 2; var targetFruitRadius = targetFruitObj.width / 2; var hitboxReduction = 0; if (activeFruitLevel > 5) { hitboxReduction += (activeFruitLevel - 5) * 3; } if (targetFruitLevel > 5) { hitboxReduction += (targetFruitLevel - 5) * 3; } var combinedRadii = activeFruitRadius + targetFruitRadius - hitboxReduction; var minDistanceSquared = combinedRadii * combinedRadii; if (distanceSquared < minDistanceSquared) { return true; } return false; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xf6e58d }); /**** * Game Code ****/ var gameOverLine; var pineapple; var pineappleActive = false; var pineapplePushCount = 0; var readyToReleaseCharged = false; var trajectoryLine; var chargedFruitIconScale = 0.3; var isClickable = true; var evolutionLine; var fireContainer; var activeFireElements = []; var BASE_FIRES = 3; var FRUITS_PER_ADDITIONAL_FIRE = 3; var FIRE_Y_START_OFFSET = 50; var FIRE_Y_STACK_OFFSET = 100; var FIRE_X_SPREAD = 500; var MAX_FIRES = 15; var fruitLevels = { 'CHERRY': 1, 'GRAPE': 2, 'APPLE': 3, 'ORANGE': 4, 'WATERMELON': 5, 'PINEAPPLE': 6, 'MELON': 7, 'PEACH': 8, 'COCONUT': 9, 'DURIAN': 10 }; function getFruitLevel(fruit) { if (!fruit || !fruit.type || !fruit.type.id) { return 10; } return fruitLevels[fruit.type.id.toUpperCase()] || 10; } function releasePineappleOnMerge() { mergeCounter++; pushPineapple(); if (mergeCounter >= 15 && !pineappleActive && pineapple) { pineappleActive = true; pineapple.isStatic = false; applyDropPhysics(pineapple, 2.5); fruits.push(pineapple); if (spatialGrid) { spatialGrid.insertObject(pineapple); } LK.setTimeout(function () { if (pineapple && fruits.includes(pineapple)) { pineapple.immuneToGameOver = false; } }, 2000); setupPineapple(); mergeCounter = 0; } } var FruitTypes = { 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' }, 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 } }; var gameWidth = 2048; var gameHeight = 2732; var fruits = []; var nextFruitType = null; var activeFruit = null; var wallLeft, wallRight, gameFloor; var dropPointY = 200; var gameOver = false; var scoreText; var isDragging = false; var chargedBallUI = null; var chargeCounter = 0; var mergeCounter = 0; var lastDroppedFruit = null; var lastDroppedHasMerged = false; var spatialGrid = null; function setupBoundaries() { wallLeft = game.addChild(LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 })); wallLeft.x = 0; wallLeft.y = gameHeight / 2; wallLeft.alpha = 0; wallRight = game.addChild(LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 })); wallRight.x = gameWidth; wallRight.y = gameHeight / 2; wallRight.alpha = 0; gameFloor = game.addChild(LK.getAsset('floor', { anchorX: 0.5, anchorY: 0.5 })); gameFloor.x = gameWidth / 2; gameFloor.y = gameHeight; gameFloor.alpha = 0; gameOverLine = game.addChild(new Line()); gameOverLine.x = gameWidth / 2; gameOverLine.y = 550; gameOverLine.scaleX = 1; gameOverLine.scaleY = 0.2; gameOverLine.alpha = 1; } function createNextFruit() { var fruitProbability = Math.random(); var fruitType = fruitProbability < 0.6 ? FruitTypes.CHERRY : FruitTypes.GRAPE; nextFruitType = fruitType; activeFruit = new Fruit(nextFruitType); activeFruit.x = lastDroppedFruit && lastDroppedFruit.x ? lastDroppedFruit.x : gameWidth / 2; activeFruit.y = dropPointY + 200; activeFruit.isStatic = true; game.addChild(activeFruit); if (trajectoryLine) { trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y); } } function dropFruit() { if (gameOver || !activeFruit || !isClickable) { return; } isClickable = false; LK.setTimeout(function () { isClickable = true; }, 300); activeFruit.isStatic = false; applyDropPhysics(activeFruit, 3.5); fruits.push(activeFruit); spatialGrid.insertObject(activeFruit); lastDroppedFruit = activeFruit; lastDroppedHasMerged = false; chargeCounter++; updateChargedBallDisplay(); if (chargeCounter >= 15 && !readyToReleaseCharged) { releaseChargedBalls(); } activeFruit.mergeGracePeriodActive = true; LK.setTimeout(function () { if (activeFruit && fruits.includes(activeFruit)) { activeFruit.mergeGracePeriodActive = false; } }, 2000); if (trajectoryLine && trajectoryLine.dots && trajectoryLine.dots.length) { for (var i = 0; i < trajectoryLine.dots.length; i++) { trajectoryLine.dots[i].visible = false; } } for (var i = 0; i < fruits.length; i++) { if (fruits[i] && fruits[i].fromChargedRelease) { fruits[i].fromChargedRelease = false; } } LK.getSound('drop').play(); if (readyToReleaseCharged && chargeCounter >= 15) { LK.getSound('pickleRick').play(); var orange = new Fruit(FruitTypes.ORANGE); var minX = wallLeft.x + wallLeft.width / 2 + orange.width / 2 + 50; var maxX = wallRight.x - wallRight.width / 2 - orange.width / 2 - 50; orange.x = minX + Math.random() * (maxX - minX); orange.y = -orange.height; orange.isStatic = false; var forceMultiplier = 3.5 + (Math.random() * 1 - 0.5); applyDropPhysics(orange, forceMultiplier); orange.fromChargedRelease = true; game.addChild(orange); fruits.push(orange); spatialGrid.insertObject(orange); chargeCounter = 0; resetChargedBalls(); readyToReleaseCharged = false; } activeFruit = null; createNextFruit(); } function applyDropPhysics(fruit, forceMultiplier) { var fruitLevel = getFruitLevel(fruit); forceMultiplier *= 1.4; var levelAdjustedForce = forceMultiplier * (1 + (fruitLevel - 1) * 0.05); var mobilityReduction = Math.max(0, (fruitLevel - 2) * 0.03); levelAdjustedForce *= 1 - mobilityReduction; var angle = (Math.random() * 20 - 10) * (Math.PI / 180); fruit.vx = Math.sin(angle) * levelAdjustedForce; fruit.vy = Math.abs(Math.cos(angle) * levelAdjustedForce) * 1.5; fruit.gravity = 13.0 * (1 + (fruitLevel - 1) * 0.2); if (fruitLevel >= 5) { fruit.gravity *= 1 + (fruitLevel - 5) * 0.03; } fruit.safetyPeriod = false; fruit.immuneToGameOver = true; LK.setTimeout(function () { if (fruit && fruits.includes(fruit)) { fruit.immuneToGameOver = false; } }, 1000); } function updateScoreDisplay() { scoreText.setText(LK.getScore()); } function setupUI() { scoreText = new Text2("0", { size: 120, fill: 0x000000 }); scoreText.anchor.set(0.5, 0); LK.gui.top.addChild(scoreText); scoreText.y = 400; setupChargedBallDisplay(); } function setupChargedBallDisplay() { chargedBallUI = new ChargedBallUI(); chargedBallUI.initialize(); game.addChild(chargedBallUI); } function updateChargedBallDisplay() { chargedBallUI && chargedBallUI.updateChargeDisplay(chargeCounter); } function releaseChargedBalls() { readyToReleaseCharged = true; chargedBallUI && chargedBallUI.setReadyState(true); } function resetChargedBalls() { chargedBallUI && chargedBallUI.reset(); } function checkFruitCollisions() { for (var k = 0; k < fruits.length; k++) { if (fruits[k]) { fruits[k].neighboringFruits = 0; fruits[k].neighborContacts = fruits[k].neighborContacts || []; fruits[k].neighborContacts = []; } } outerLoop: for (var i = fruits.length - 1; i >= 0; i--) { var fruit1 = fruits[i]; if (!fruit1 || fruit1 === activeFruit || fruit1.merging || fruit1.isStatic) { continue; } var candidates = spatialGrid.getPotentialCollisions(fruit1); for (var j = 0; j < candidates.length; j++) { var fruit2 = candidates[j]; if (!fruit2 || fruit2 === activeFruit || fruit2.merging || fruit2.isStatic || fruit1 === fruit2) { continue; } if (fruits.indexOf(fruit2) === -1) { continue; } var dx = fruit2.x - fruit1.x; var dy = fruit2.y - fruit1.y; var distanceSquared = dx * dx + dy * dy; var distance = Math.sqrt(distanceSquared); var fruit1HalfWidth = fruit1.width / 2; var fruit1HalfHeight = fruit1.height / 2; var fruit2HalfWidth = fruit2.width / 2; var fruit2HalfHeight = fruit2.height / 2; var absDistanceX = Math.abs(dx); var absDistanceY = Math.abs(dy); var level1 = getFruitLevel(fruit1); var level2 = getFruitLevel(fruit2); var hitboxReduction = 0; if (level1 > 5) { hitboxReduction += (level1 - 5) * 3; } if (level2 > 5) { hitboxReduction += (level2 - 5) * 3; } var combinedHalfWidths = fruit1HalfWidth + fruit2HalfWidth - hitboxReduction / 2; var combinedHalfHeights = fruit1HalfHeight + fruit2HalfHeight - hitboxReduction / 2; var colliding = absDistanceX < combinedHalfWidths && absDistanceY < combinedHalfHeights; if (colliding) { if (fruit1.type === fruit2.type) { fruit1.merge(fruit2); continue outerLoop; } else { if (distance === 0) { distance = 0.1; dx = distance; dy = 0; } var overlap = combinedHalfWidths - absDistanceX; if (absDistanceY < combinedHalfHeights && absDistanceX < combinedHalfWidths) { overlap = Math.min(combinedHalfWidths - absDistanceX, combinedHalfHeights - absDistanceY); } var normalizeX = dx / distance; var normalizeY = dy / distance; var moveX = overlap / 2 * normalizeX; var moveY = overlap / 2 * normalizeY; var separationFactor = 1.05; fruit1.x -= moveX * separationFactor; fruit1.y -= moveY * separationFactor; fruit2.x += moveX * separationFactor; fruit2.y += moveY * separationFactor; var rvX = fruit2.vx - fruit1.vx; var rvY = fruit2.vy - fruit1.vy; var contactVelocity = rvX * normalizeX + rvY * normalizeY; if (contactVelocity < 0) { var collisionElasticity = Math.max(fruit1.elasticity, fruit2.elasticity); var impulse = -(1 + collisionElasticity) * contactVelocity; var level1 = getFruitLevel(fruit1); var level2 = getFruitLevel(fruit2); var mass1 = Math.pow(level1, 2.0) * Math.pow(fruit1.type.size, 1.5); var mass2 = Math.pow(level2, 2.0) * Math.pow(fruit2.type.size, 1.5); var totalMass = mass1 + mass2; var impulseRatio1 = totalMass > 0 ? mass2 / totalMass : 0.5; var impulseRatio2 = totalMass > 0 ? mass1 / totalMass : 0.5; var levelDifference = Math.abs(level1 - level2); var levelImpactMultiplier = 1 + levelDifference * 0.03; var maxImpulseRatio = 1.7; if (level1 > level2) { impulseRatio2 = Math.min(impulseRatio2 * levelImpactMultiplier * 1.3, maxImpulseRatio * impulseRatio2); } else if (level2 > level1) { impulseRatio1 = Math.min(impulseRatio1 * levelImpactMultiplier * 1.3, maxImpulseRatio * impulseRatio1); } var velocityMagnitude = Math.sqrt(rvX * rvX + rvY * rvY); var velocityDampening = 1.0; if (velocityMagnitude > 5) { velocityDampening = 0.5 + 0.5 * (5 / velocityMagnitude); } var impulse1 = impulse * impulseRatio1 * velocityDampening; var impulse2 = impulse * impulseRatio2 * velocityDampening; var sizeDifference = Math.abs(fruit1.type.size - fruit2.type.size) / Math.max(fruit1.type.size, fruit2.type.size, 1); if (fruit1.type.size < fruit2.type.size) { impulse1 *= 1 + sizeDifference * 2.5; fruit1._lastBounceTime = Date.now(); } else if (fruit2.type.size < fruit1.type.size) { impulse2 *= 1 + sizeDifference * 2.5; fruit2._lastBounceTime = Date.now(); } fruit1.vx -= impulse1 * normalizeX; fruit1.vy -= impulse1 * normalizeY; fruit2.vx += impulse2 * normalizeX; fruit2.vy += impulse2 * normalizeY; var tangentX = -normalizeY; var tangentY = normalizeX; var tangentVelocity = rvX * tangentX + rvY * tangentY; var frictionImpulse = -tangentVelocity * 0.1; fruit1.vx -= frictionImpulse * tangentX * impulseRatio1; fruit1.vy -= frictionImpulse * tangentY * impulseRatio1; fruit2.vx += frictionImpulse * tangentX * impulseRatio2; fruit2.vy += frictionImpulse * tangentY * impulseRatio2; var rotationTransferFactor = 0.01; var tangentialComponent = rvX * tangentX + rvY * tangentY; var inertia1 = mass1 * Math.pow(fruit1.width / 2, 2); var inertia2 = mass2 * Math.pow(fruit2.width / 2, 2); var angularImpulse = tangentialComponent * rotationTransferFactor; fruit1.angularVelocity += inertia2 > 0 ? angularImpulse * (inertia1 / (inertia1 + inertia2)) : angularImpulse * 0.5; fruit2.angularVelocity -= inertia1 > 0 ? angularImpulse * (inertia2 / (inertia1 + inertia2)) : angularImpulse * 0.5; fruit1.angularVelocity *= 0.95; fruit2.angularVelocity *= 0.95; fruit1.angularVelocity = Math.min(Math.max(fruit1.angularVelocity, -fruit1.maxAngularVelocity), fruit1.maxAngularVelocity); fruit2.angularVelocity = Math.min(Math.max(fruit2.angularVelocity, -fruit2.maxAngularVelocity), fruit2.maxAngularVelocity); fruit1.neighboringFruits = (fruit1.neighboringFruits || 0) + 1; fruit2.neighboringFruits = (fruit2.neighboringFruits || 0) + 1; fruit1.neighborContacts = fruit1.neighborContacts || []; fruit2.neighborContacts = fruit2.neighborContacts || []; if (fruit1.neighborContacts.indexOf(fruit2.id) === -1) { fruit1.neighborContacts.push(fruit2.id); } if (fruit2.neighborContacts.indexOf(fruit1.id) === -1) { fruit2.neighborContacts.push(fruit1.id); } var currentTime = Date.now(); fruit1._collisionHistory = fruit1._collisionHistory || {}; fruit2._collisionHistory = fruit2._collisionHistory || {}; var recentCollision = false; if (fruit1._collisionHistory[fruit2.id] && currentTime - fruit1._collisionHistory[fruit2.id] < 300) { recentCollision = true; } fruit1._collisionHistory[fruit2.id] = currentTime; fruit2._collisionHistory[fruit1.id] = currentTime; if (recentCollision) { fruit1.vx *= 0.7; fruit1.vy *= 0.7; fruit2.vx *= 0.7; fruit2.vy *= 0.7; fruit1.angularVelocity *= 0.6; fruit2.angularVelocity *= 0.6; } if (fruit1.neighborContacts.length >= 3 || fruit2.neighborContacts.length >= 3) { var stabilizeFactor = 0.6; fruit1.vx *= stabilizeFactor; fruit1.vy *= stabilizeFactor; fruit2.vx *= stabilizeFactor; fruit2.vy *= stabilizeFactor; fruit1.angularVelocity *= 0.75; fruit2.angularVelocity *= 0.75; } } } } } } } 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) { continue; } var fruitHalfHeight = fruit.height / 2; var fruitHalfWidth = fruit.width / 2; var fruitLevel = getFruitLevel(fruit); var sizeReduction = 0; if (fruitLevel > 5) { sizeReduction = (fruitLevel - 5) * 3; } fruitHalfHeight = Math.max(10, fruitHalfHeight - sizeReduction / 2); fruitHalfWidth = Math.max(10, fruitHalfWidth - sizeReduction / 2); var cosAngle = Math.abs(Math.cos(fruit.rotation)); var sinAngle = Math.abs(Math.sin(fruit.rotation)); var effectiveHeight = fruitHalfHeight * cosAngle + fruitHalfWidth * sinAngle; var fruitTopY = fruit.y - effectiveHeight; var lineBottomY = gameOverLine.y + gameOverLine.height / 2; if (fruitTopY <= lineBottomY) { var effectiveWidth = fruitHalfWidth * cosAngle + fruitHalfHeight * sinAngle; var fruitLeftX = fruit.x - effectiveWidth; var fruitRightX = fruit.x + effectiveWidth; var lineLeftX = gameOverLine.x - gameOverLine.width * gameOverLine.scaleX / 2; var lineRightX = gameOverLine.x + gameOverLine.width * gameOverLine.scaleX / 2; var horizontalOverlap = !(fruitRightX < lineLeftX || fruitLeftX > lineRightX); if (horizontalOverlap) { if (fruit.immuneToGameOver) { continue; } var stableOrSlowing = Math.abs(fruit.vy) < 1.0 || fruit._boundaryContacts && (fruit._boundaryContacts.left || fruit._boundaryContacts.right || fruit._boundaryContacts.floor); if (stableOrSlowing) { gameOver = true; LK.showGameOver(); return; } } } else { fruit.safetyPeriod = undefined; } } } function setupPineapple() { pineapple = new Fruit(FruitTypes.PINEAPPLE); pineapple.x = -pineapple.width / 2; pineapple.y = 200; pineapple.isStatic = true; pineappleActive = false; pineapplePushCount = 0; game.addChild(pineapple); } function pushPineapple() { if (!pineappleActive && pineapple) { var step = mergeCounter; var totalSteps = 15; var percentage = Math.min(step / totalSteps, 1.0); var startPos = -pineapple.width / 2; var endPos = gameWidth * 0.16; var newX = startPos + percentage * (endPos - startPos); tween(pineapple, { x: newX }, { duration: 300, easing: tween.bounceOut }); } } function initGame() { LK.setScore(0); gameOver = false; fruits = []; chargeCounter = 0; if (chargedBallUI) { chargedBallUI.destroy(); } chargedBallUI = null; readyToReleaseCharged = false; lastScoreCheckForCoconut = 0; lastDroppedFruit = null; lastDroppedHasMerged = false; mergeCounter = 0; isClickable = true; // Initialize fire background if (fireContainer) { // Clean up existing fire elements for (var i = 0; i < activeFireElements.length; i++) { if (activeFireElements[i]) { activeFireElements[i].destroy(); } } fireContainer.destroy(); } fireContainer = new Container(); game.addChildAt(fireContainer, 0); // Add at bottom layer activeFireElements = []; if (spatialGrid) { spatialGrid.clear(); } else { var avgFruitSize = 0; var fruitCount = 0; for (var fruitType in FruitTypes) { if (FruitTypes.hasOwnProperty(fruitType)) { avgFruitSize += FruitTypes[fruitType].size; fruitCount++; } } avgFruitSize = fruitCount > 0 ? avgFruitSize / fruitCount : 300; var optimalCellSize = Math.ceil(avgFruitSize * 1.1); spatialGrid = new SpatialGrid(optimalCellSize); } LK.playMusic('bgmusic'); if (wallLeft) { wallLeft.destroy(); } if (wallRight) { wallRight.destroy(); } if (gameFloor) { gameFloor.destroy(); } if (gameOverLine) { gameOverLine.destroy(); } if (pineapple) { pineapple.destroy(); } if (trajectoryLine) { trajectoryLine.destroy(); } if (evolutionLine) { evolutionLine.destroy(); } setupBoundaries(); setupUI(); setupPineapple(); updateFireBackground(); // Initial setup of fire elements trajectoryLine = game.addChild(new TrajectoryLine()); trajectoryLine.createDots(); evolutionLine = game.addChild(new EvolutionLine()); evolutionLine.initialize(); updateScoreDisplay(); activeFruit = null; createNextFruit(); resetChargedBalls(); updateFireBackground(); // Initial setup of fire elements } function spawnCoconut() { var coconut = new Fruit(FruitTypes.COCONUT); var minX = wallLeft.x + wallLeft.width / 2 + coconut.width / 2 + 50; var maxX = wallRight.x - wallRight.width / 2 - coconut.width / 2 - 50; coconut.x = minX + Math.random() * (maxX - minX); coconut.y = gameHeight + coconut.height / 2; coconut.isStatic = true; game.addChild(coconut); fruits.push(coconut); coconut.safetyPeriod = false; coconut.immuneToGameOver = true; var targetY = gameHeight - gameFloor.height / 2 - coconut.height / 2 - 10; tween(coconut, { y: targetY }, { duration: 1200, easing: tween.easeIn, onFinish: function onFinish() { if (!coconut || !fruits.includes(coconut)) { return; } coconut.isStatic = false; coconut.vy = -2; coconut.vx = (Math.random() * 2 - 1) * 1.5; spatialGrid.insertObject(coconut); LK.setTimeout(function () { if (coconut && fruits.includes(coconut)) { coconut.immuneToGameOver = false; } }, 1000); } }); } var lastScoreCheckForCoconut = 0; game.down = function (x, y) { if (activeFruit && !gameOver) { isDragging = true; game.move(x, y); } }; game.move = function (x, y) { if (isDragging && activeFruit && !gameOver) { var fruitRadius = activeFruit.width / 2; 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)); activeFruit.y = dropPointY + 200; if (trajectoryLine) { trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y); } } }; game.up = function () { if (isDragging && activeFruit && isClickable && !gameOver) { dropFruit(); } isDragging = false; }; function updatePhysics() { if (spatialGrid && Date.now() - spatialGrid.lastRebuildTime > spatialGrid.rebuildInterval) { spatialGrid.rebuildGrid(fruits); } for (var i = fruits.length - 1; i >= 0; i--) { var fruit = fruits[i]; if (!fruit || fruit.isStatic || fruit.merging) { continue; } fruit.updatePhysics(); var walls = { left: wallLeft, right: wallRight }; fruit.checkBoundaries(walls, gameFloor); } checkFruitCollisions(); for (var i = 0; i < fruits.length; i++) { var fruit = fruits[i]; if (fruit && !fruit.isStatic && !fruit.merging) { if (fruit.surroundedFrames === undefined) { fruit.surroundedFrames = 0; } var boundaryContact = fruit.wallContactFrames > 0; var floorProximity = fruit.y + fruit.height / 2 >= gameFloor.y - gameFloor.height / 2 - 10; var hasMultipleNeighbors = fruit.neighborContacts && fruit.neighborContacts.length >= 2; var isSlowMoving = Math.abs(fruit.vx) < 0.3 && Math.abs(fruit.vy) < 0.3; var hasGapBelow = false; if (floorProximity || hasMultipleNeighbors) { var potentialNeighbors = spatialGrid.getPotentialCollisions({ x: fruit.x, y: fruit.y + fruit.height / 2 + 20, width: fruit.width * 0.6, height: 20, id: 'gap_check_' + fruit.id }); hasGapBelow = potentialNeighbors.length === 0 && !floorProximity; if (hasGapBelow && isSlowMoving) { var leftCount = 0, rightCount = 0; for (var j = 0; j < fruit.neighborContacts.length; j++) { var neighborId = fruit.neighborContacts[j]; for (var k = 0; k < fruits.length; k++) { if (fruits[k] && fruits[k].id === neighborId) { if (fruits[k].x < fruit.x) { leftCount++; } else if (fruits[k].x > fruit.x) { rightCount++; } break; } } } if (leftCount < rightCount) { fruit.vx -= 0.05; } else if (rightCount < leftCount) { fruit.vx += 0.05; } else { fruit.vx += Math.random() * 0.1 - 0.05; } } } var stabilizationScenario = boundaryContact ? "boundary" : floorProximity ? "floor" : hasMultipleNeighbors ? "surrounded" : "free"; switch (stabilizationScenario) { case "boundary": fruit.surroundedFrames = 0; break; case "floor": fruit.surroundedFrames = 0; break; case "surrounded": fruit.surroundedFrames++; var stabilizationStrength = Math.min(0.92, 0.75 + fruit.surroundedFrames * 0.015); if (fruit.neighborContacts.length >= 3) { stabilizationStrength = Math.min(0.95, stabilizationStrength + 0.03); } fruit.vx *= stabilizationStrength; fruit.vy *= stabilizationStrength; fruit.angularVelocity *= stabilizationStrength; if (fruit.surroundedFrames > 10 && isSlowMoving || fruit.surroundedFrames > 18) { if (Math.abs(fruit.vx) < 0.05) { fruit.vx = 0; } if (Math.abs(fruit.vy) < 0.05) { fruit.vy = 0; } if (Math.abs(fruit.angularVelocity) < 0.005) { fruit.angularVelocity = 0; } } break; case "free": fruit.surroundedFrames = 0; break; } spatialGrid.updateObject(fruit); } } } game.update = function () { if (gameOver) { return; } var currentScore = LK.getScore(); if (currentScore >= lastScoreCheckForCoconut + 1000) { lastScoreCheckForCoconut = Math.floor(currentScore / 1000) * 1000; spawnCoconut(); } else { if (currentScore > lastScoreCheckForCoconut) { lastScoreCheckForCoconut = Math.floor(currentScore / 1000) * 1000; } } updatePhysics(); checkGameOver(); // Update fire background updateFireBackground(); // Animate individual fire elements for (var i = 0; i < activeFireElements.length; i++) { if (activeFireElements[i]) { activeFireElements[i].update(); } } }; initGame(); function updateFireBackground() { // Track unique fruit types on the board var uniqueFruitTypes = {}; for (var i = 0; i < fruits.length; i++) { var fruit = fruits[i]; if (fruit && !fruit.isStatic && !fruit.merging && fruit.type && fruit.type.id) { uniqueFruitTypes[fruit.type.id] = true; } } // Count the number of unique fruit types var uniqueTypeCount = Object.keys(uniqueFruitTypes).length; // Calculate target number of fire elements: BASE_FIRES + one for each unique fruit type var targetFireCount = BASE_FIRES + uniqueTypeCount; targetFireCount = Math.min(targetFireCount, MAX_FIRES); // Create or remove fires to match target count if (targetFireCount > activeFireElements.length) { // Need to add more fires var newFiresCount = targetFireCount - activeFireElements.length; for (var i = 0; i < newFiresCount; i++) { // Create fire at appropriate position createFireElement(activeFireElements.length); } } else if (targetFireCount < activeFireElements.length) { // Need to remove excess fires var fireToRemoveCount = activeFireElements.length - targetFireCount; for (var i = 0; i < fireToRemoveCount; i++) { var fire = activeFireElements.pop(); // Remove from end (highest/last added fires) if (fire) { fire.destroy(); // Ensure destroy() is called to clean up resources fireContainer.removeChild(fire); } } } } // Helper function to create a single fire element at the specified index function createFireElement(index) { // Calculate y position (higher fires appear behind/further back) var yPos = gameHeight + FIRE_Y_START_OFFSET - index * FIRE_Y_STACK_OFFSET; // Randomize x position around center of screen var xPos = gameWidth / 2 + (Math.random() * FIRE_X_SPREAD - FIRE_X_SPREAD / 2); // Create new fire element var newFire = new FireElement(xPos, yPos); // Alternate x-axis flip for all fires including the first three if (index % 2 === 1) { newFire.fireAsset.scaleX = -1; // Flip every other fire } // Add at index 0 to render behind existing fires fireContainer.addChildAt(newFire, 0); activeFireElements.push(newFire); } function removeFruitFromGame(fruit) { var index = fruits.indexOf(fruit); if (index !== -1) { fruits.splice(index, 1); } spatialGrid.removeObject(fruit); fruit.destroy(); }
/****
* 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 = 15;
self.currentCharge = 0;
self.isReadyToRelease = false;
self.countdownText = null;
self.pulseAnimationActive = false;
self.initialize = function () {
self.portalAsset = self.attachAsset('portal', {
anchorX: 0.5,
anchorY: 0.5
});
self.portalAsset.x = gameWidth / 2 + 870;
self.portalAsset.alpha = 0; // Start invisible
self.portalAsset.scaleX = 0;
self.portalAsset.scaleY = 0;
self.y = 120;
};
self.updateChargeDisplay = function (chargeCount) {
self.currentCharge = chargeCount;
var remainingCount = Math.max(0, self.chargeNeededForRelease - self.currentCharge);
// No hue/tint changes for portal - keep original appearance
// Calculate size based on charge progress
var progressPercent = (self.chargeNeededForRelease - remainingCount) / self.chargeNeededForRelease;
var targetScale = progressPercent;
// Make portal visible once charging starts
if (progressPercent > 0 && self.portalAsset.alpha === 0) {
self.portalAsset.alpha = 1;
}
tween(self.portalAsset, {
scaleX: targetScale,
scaleY: targetScale,
alpha: progressPercent // Gradually increase visibility
}, {
duration: 300,
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 // Make portal spin clockwise 360 degrees
}, {
duration: 300,
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: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isReadyToRelease) {
self.pulseAnimationActive = false;
return;
}
tween(self.portalAsset, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 500,
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
});
};
return self;
});
var CollisionComponent = Container.expand(function () {
var self = Container.call(this);
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;
var fruitLevel = getFruitLevel(fruit);
var sizeReduction = 0;
if (fruitLevel > 5) {
sizeReduction = (fruitLevel - 5) * 3;
}
fruitHalfWidth = Math.max(10, fruitHalfWidth - sizeReduction / 2);
fruitHalfHeight = Math.max(10, fruitHalfHeight - sizeReduction / 2);
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;
var wasInContact = fruit.wallContactFrames > 0;
fruit._boundaryContacts = fruit._boundaryContacts || {
left: false,
right: false,
floor: false
};
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 isInContact = fruit._boundaryContacts.left || fruit._boundaryContacts.right || fruit._boundaryContacts.floor;
if (isInContact) {
fruit.wallContactFrames++;
var progressiveFriction = Math.min(0.85, 0.65 + fruit.wallContactFrames * 0.01);
fruit.angularVelocity *= progressiveFriction;
if (fruit.wallContactFrames > 10) {
fruit.vx *= 0.9;
fruit.vy *= 0.9;
}
} else {
fruit.wallContactFrames = Math.max(0, fruit.wallContactFrames - 1);
}
};
self.checkLeftWallCollision = function (fruit, leftWall, effectiveWidth) {
var leftBoundary = leftWall.x + leftWall.width / 2 + effectiveWidth;
if (fruit.x < leftBoundary) {
fruit.x = leftBoundary;
fruit.vx = -fruit.vx * fruit.elasticity * 0.7;
var angularImpactMultiplier = 0.0025 * (1 + (0.9 - fruit.elasticity) * 5);
fruit.angularVelocity += fruit.vy * angularImpactMultiplier * 0.25;
fruit.angularVelocity *= fruit.groundAngularFriction * 0.8;
fruit._boundaryContacts.left = true;
if (fruit.wallContactFrames > 3 && Math.abs(fruit.vx) < 0.8) {
fruit.vx = 0;
}
} else if (fruit.x > leftWall.x + leftWall.width * 2) {
if (fruit._boundaryContacts && !fruit._boundaryContacts.right && !fruit._boundaryContacts.floor) {
fruit.wallContactFrames = Math.max(0, fruit.wallContactFrames - 1);
}
}
};
self.checkRightWallCollision = function (fruit, rightWall, effectiveWidth) {
var rightBoundary = rightWall.x - rightWall.width / 2 - effectiveWidth;
if (fruit.x > rightBoundary) {
fruit.x = rightBoundary;
fruit.vx = -fruit.vx * fruit.elasticity * 0.7;
var angularImpactMultiplier = 0.0025 * (1 + (0.9 - fruit.elasticity) * 5);
fruit.angularVelocity -= fruit.vy * angularImpactMultiplier * 0.25;
fruit.angularVelocity *= fruit.groundAngularFriction * 0.8;
fruit._boundaryContacts.right = true;
if (fruit.wallContactFrames > 3 && Math.abs(fruit.vx) < 0.8) {
fruit.vx = 0;
}
}
};
self.checkFloorCollision = function (fruit, floor, effectiveHeight) {
var floorCollisionY = floor.y - floor.height / 2 - effectiveHeight;
if (fruit.y > floorCollisionY) {
fruit.y = floorCollisionY;
var oldVy = fruit.vy;
fruit.vy = -fruit.vy * fruit.elasticity * 0.5;
fruit._boundaryContacts.floor = true;
if (Math.abs(fruit.vx) > 0.5) {
fruit.angularVelocity = fruit.vx * 0.01;
}
fruit.angularVelocity *= fruit.groundAngularFriction * 0.8;
var restThreshold = fruit.wallContactFrames > 5 ? 1.5 : 2.5;
if (Math.abs(fruit.vy) < restThreshold) {
fruit.vy = 0;
fruit.vx *= 0.7;
if (fruit.wallContactFrames > 8) {
fruit.vx *= 0.85;
}
}
var angularRestThreshold = fruit.wallContactFrames > 5 ? 0.015 : 0.025;
if (Math.abs(fruit.angularVelocity) < angularRestThreshold) {
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 = 120;
self.x = gameWidth / 2 + 350; // Moved 300 pixels to the right
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(150 / fruitIcon.width, 150 / fruitIcon.height);
fruitIcon.scaleX = scale;
fruitIcon.scaleY = scale;
totalWidth += fruitIcon.width * scale;
if (i < fruitTypes.length - 1) {
totalWidth += 100; // Increased 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 + 20; // Increased spacing
}
};
return self;
});
var FireElement = Container.expand(function (initX, initY, zIndex) {
var self = Container.call(this);
// Configuration
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;
// Alpha range changed to 0.1-0.5
self.alphaMin = 0.1;
self.alphaMax = 0.5;
self.flickerSpeed = 500 + Math.random() * 300;
self.frameIndex = 0;
self.frameTimer = null;
self.frameDuration = 200; // Switch frames every 300ms
// Setup
self.initialize = function () {
// Create both fire frames (one visible, one hidden initially)
self.fireAsset = self.attachAsset('fire', {
anchorX: 0.5,
anchorY: 1.0 // Anchor at bottom-center
});
self.fireAsset2 = self.attachAsset('fire_2', {
anchorX: 0.5,
anchorY: 1.0 // Anchor at bottom-center
});
self.fireAsset2.visible = false; // Start with frame 2 hidden
self.x = self.baseX;
self.y = self.baseY;
// Start animations
self.startAlphaFlicker();
self.startFrameAnimation();
};
// Horizontal sway animation
self.update = function () {
// Update horizontal position with sway animation
self.x += self.movementSpeed * self.direction;
// Check if we need to change direction
if (Math.abs(self.x - self.baseX) > self.movementRange) {
self.direction *= -1;
}
};
// Frame animation
self.startFrameAnimation = function () {
// Clear any existing frame timer
if (self.frameTimer) {
LK.clearInterval(self.frameTimer);
}
// Create new interval for frame switching
self.frameTimer = LK.setInterval(function () {
self.toggleFrame();
}, self.frameDuration);
};
self.toggleFrame = function () {
// Switch between frames
self.frameIndex = (self.frameIndex + 1) % 2;
self.fireAsset.visible = self.frameIndex === 0;
self.fireAsset2.visible = self.frameIndex === 1;
};
// Alpha flicker animation
self.startAlphaFlicker = function () {
if (self.flickerTween) {
self.flickerTween.stop();
}
// Randomize starting delay to avoid synchronized flickers
var startDelay = Math.random() * 500;
LK.setTimeout(function () {
self.flickerToMax();
}, startDelay);
};
self.flickerToMax = function () {
// Apply alpha flicker to both frames
self.flickerTween = tween(self, {
alpha: self.alphaMax // Use class variable instead of hardcoded value
}, {
duration: self.flickerSpeed,
easing: tween.easeInOut,
onFinish: self.flickerToMin
});
};
self.flickerToMin = function () {
// Apply alpha flicker to both frames
self.flickerTween = tween(self, {
alpha: self.alphaMin // Use class variable instead of hardcoded value
}, {
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);
};
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;
var physics = new PhysicsComponent();
var collision = new CollisionComponent();
var mergeHandler = new MergeComponent();
var behaviorSystem = new FruitBehavior();
self.vx = physics.vx;
self.vy = physics.vy;
self.rotation = physics.rotation;
self.angularVelocity = physics.angularVelocity;
self.angularFriction = physics.angularFriction;
self.groundAngularFriction = physics.groundAngularFriction;
var currentLevel = getFruitLevel(self);
self.gravity = physics.gravity * (1 + (currentLevel - 1) * 0.15);
self.friction = physics.friction;
self.rotationRestCounter = physics.rotationRestCounter;
self.maxAngularVelocity = physics.maxAngularVelocity;
self.isStatic = physics.isStatic;
self.elasticity = currentLevel <= 3 ? 0.85 : 0.8 - (currentLevel - 1) * (0.2 / 9);
self.wallContactFrames = collision.wallContactFrames;
self.merging = mergeHandler.merging;
self.mergeGracePeriodActive = mergeHandler.mergeGracePeriodActive;
self.fromChargedRelease = mergeHandler.fromChargedRelease;
self.safetyPeriod = false;
self.immuneToGameOver = false;
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.log("Warning: Fruit type not available yet or missing required properties");
}
self.updatePhysics = function () {
physics.apply(self);
};
self.checkBoundaries = function (walls, floor) {
collision.checkBoundaryCollisions(self, walls, floor);
if (self.safetyPeriod === false && self.vy <= 0.1) {
self.safetyPeriod = undefined;
}
};
self.merge = function (otherFruit) {
mergeHandler.beginMerge(self, otherFruit);
};
return self;
});
var FruitBehavior = Container.expand(function () {
var self = Container.call(this);
self.behaviors = {
CHERRY: {
onMerge: function onMerge(fruit1, fruit2, posX, posY) {
return self.standardMerge(fruit1, fruit2, posX, posY);
}
},
GRAPE: {
onMerge: function onMerge(fruit1, fruit2, posX, posY) {
return self.standardMerge(fruit1, fruit2, posX, posY);
}
},
APPLE: {
onMerge: function onMerge(fruit1, fruit2, posX, posY) {
return self.standardMerge(fruit1, fruit2, posX, posY);
}
},
ORANGE: {
onMerge: function onMerge(fruit1, fruit2, posX, posY) {
return self.standardMerge(fruit1, fruit2, posX, posY);
}
},
WATERMELON: {
onMerge: function onMerge(fruit1, fruit2, posX, posY) {
return self.standardMerge(fruit1, fruit2, posX, posY);
}
},
PINEAPPLE: {
onMerge: function onMerge(fruit1, fruit2, posX, posY) {
return self.standardMerge(fruit1, fruit2, posX, posY);
},
onSpawn: function onSpawn() {}
},
MELON: {
onMerge: function onMerge(fruit1, fruit2, posX, posY) {
LK.getSound('Smartz').play();
return self.standardMerge(fruit1, fruit2, posX, posY);
}
},
PEACH: {
onMerge: function onMerge(fruit1, fruit2, posX, posY) {
LK.getSound('stonks').play();
return self.standardMerge(fruit1, fruit2, posX, posY);
}
},
COCONUT: {
onMerge: function onMerge(fruit1, fruit2, posX, posY) {
LK.getSound('ThisIsFine').play();
return self.standardMerge(fruit1, fruit2, posX, posY);
},
onSpawn: function onSpawn() {
LK.getSound('stonks').play();
}
},
DURIAN: {
onMerge: function onMerge(fruit1, fruit2, posX, posY) {
LK.setScore(LK.getScore() + fruit1.type.points);
updateScoreDisplay();
removeFruitFromGame(fruit1);
removeFruitFromGame(fruit2);
releasePineappleOnMerge();
return null;
}
}
};
self.getMergeHandler = function (fruitTypeId) {
if (!fruitTypeId) {
return self.behaviors.CHERRY;
}
var upperTypeId = fruitTypeId.toUpperCase();
return self.behaviors[upperTypeId] || self.behaviors.CHERRY;
};
self.standardMerge = function (fruit1, fruit2, posX, posY) {
var nextType = FruitTypes[fruit1.type.next.toUpperCase()];
releasePineappleOnMerge();
return self.createNextLevelFruit(fruit1, nextType, posX, posY);
};
self.createNextLevelFruit = function (sourceFruit, nextType, posX, posY) {
var newFruit = new Fruit(nextType);
newFruit.x = posX;
newFruit.y = posY;
newFruit.scaleX = 0.5;
newFruit.scaleY = 0.5;
game.addChild(newFruit);
fruits.push(newFruit);
spatialGrid.insertObject(newFruit);
LK.setScore(LK.getScore() + nextType.points);
updateScoreDisplay();
tween(newFruit, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
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', {
anchorX: 0.5,
anchorY: 0.5
});
lineGraphics.tint = 0xff0000;
lineGraphics.height = 20;
return self;
});
var MergeComponent = Container.expand(function () {
var self = Container.call(this);
self.merging = false;
self.mergeGracePeriodActive = false;
self.fromChargedRelease = false;
self.fruitBehavior = new FruitBehavior();
self.beginMerge = function (fruit1, fruit2) {
if (fruit1.merging) {
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) {
tween(fruit1, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 200,
easing: tween.easeOut
});
tween(fruit2, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.completeMerge(fruit1, fruit2, midX, midY);
}
});
};
self.completeMerge = function (fruit1, fruit2, midX, midY) {
LK.getSound('merge').play();
self.trackMerge(fruit1, fruit2);
var behaviorHandler = self.fruitBehavior.getMergeHandler(fruit1.type.id);
behaviorHandler.onMerge(fruit1, fruit2, midX, midY);
if (fruit1 && fruits.includes(fruit1) && fruit1.type.id.toUpperCase() !== 'DURIAN') {
removeFruitFromGame(fruit1);
}
if (fruit2 && fruits.includes(fruit2)) {
removeFruitFromGame(fruit2);
}
};
self.trackMerge = function (fruit1, fruit2) {
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);
self.vx = 0;
self.vy = 0;
self.gravity = 5.0;
self.friction = 0.92;
self.elasticity = 0.4;
self.isStatic = false;
self.rotation = 0;
self.angularVelocity = 0;
self.angularFriction = 0.85;
self.groundAngularFriction = 0.6;
self.maxAngularVelocity = 0.08;
self.rotationRestCounter = 0;
self.lastVx = 0;
self.lastVy = 0;
self.stabilizeFruit = function (fruit, movementMagnitude, velocityChange) {
var shouldStabilize = false;
var stabilizationLevel = 0;
var fruitLevel = getFruitLevel(fruit);
if (!fruit._stabilizeMetrics) {
fruit._stabilizeMetrics = {
consecutiveSmallMovements: 0,
wallContactDuration: 0,
surroundedDuration: 0,
restingDuration: 0
};
}
var massFactor = Math.pow(fruitLevel, 1.5) / 10;
var movementThreshold = 0.4 - (fruitLevel - 1) * 0.03;
var angularThreshold = 0.04 - (fruitLevel - 1) * 0.003;
if (movementMagnitude < movementThreshold && Math.abs(fruit.angularVelocity) < angularThreshold) {
fruit._stabilizeMetrics.consecutiveSmallMovements += 1 + massFactor * 0.2;
fruit._stabilizeMetrics.restingDuration += 1 + massFactor * 0.2;
} else {
fruit._stabilizeMetrics.consecutiveSmallMovements = 0;
fruit._stabilizeMetrics.restingDuration = Math.max(0, fruit._stabilizeMetrics.restingDuration - 2);
}
if (fruit.wallContactFrames > 0) {
fruit._stabilizeMetrics.wallContactDuration += 1 + massFactor * 0.15;
} else {
fruit._stabilizeMetrics.wallContactDuration = Math.max(0, fruit._stabilizeMetrics.wallContactDuration - 1);
}
if (fruit.surroundedFrames > 0) {
fruit._stabilizeMetrics.surroundedDuration += 1 + massFactor * 0.25;
} else {
fruit._stabilizeMetrics.surroundedDuration = Math.max(0, fruit._stabilizeMetrics.surroundedDuration - 1);
}
var levelFactor = (fruitLevel - 1) * 0.12;
var totalStabilizationScore = fruit._stabilizeMetrics.consecutiveSmallMovements * (1.0 + levelFactor) + fruit._stabilizeMetrics.wallContactDuration * (0.8 + levelFactor) + fruit._stabilizeMetrics.surroundedDuration * (1.2 + levelFactor) + fruit._stabilizeMetrics.restingDuration * (0.5 + levelFactor);
var fullStabilizationThreshold = 15 - (fruitLevel - 1) * 1.0;
var partialStabilizationThreshold = 8 - (fruitLevel - 1) * 0.6;
if (totalStabilizationScore > fullStabilizationThreshold) {
stabilizationLevel = 2;
} else if (totalStabilizationScore > partialStabilizationThreshold) {
stabilizationLevel = 1;
}
if (stabilizationLevel > 0) {
var baseDampFactor = stabilizationLevel === 2 ? 0.6 : 0.85;
var massDampFactor = baseDampFactor - massFactor * 0.02;
var dampFactor = Math.max(0.4, massDampFactor);
fruit.vx *= dampFactor;
fruit.vy *= dampFactor;
fruit.angularVelocity *= dampFactor;
var stopThreshold = 0.1 - (fruitLevel - 1) * 0.005;
if (stabilizationLevel === 2 && movementMagnitude < stopThreshold) {
fruit.vx = 0;
fruit.vy = 0;
fruit.angularVelocity = 0;
return true;
}
}
return false;
};
self.apply = function (fruit) {
if (fruit.isStatic || fruit.merging) {
return;
}
fruit.lastVx = fruit.vx;
fruit.lastVy = fruit.vy;
var fruitLevel = getFruitLevel(fruit);
var gravityMultiplier = 1 + (fruitLevel - 1) * 0.25;
fruit.vy += fruit.gravity * gravityMultiplier * 5;
var fruitLevel = getFruitLevel(fruit);
var maxVelocity = 60 - Math.min(8, (fruitLevel - 1) * 0.8);
if (Math.abs(fruit.vx) > maxVelocity) {
fruit.vx = fruit.vx > 0 ? maxVelocity : -maxVelocity;
}
if (Math.abs(fruit.vy) > maxVelocity) {
fruit.vy = fruit.vy > 0 ? maxVelocity : -maxVelocity;
}
if (fruitLevel > 3) {
var bottomTendencyForce = (fruitLevel - 3) * 0.018;
fruit.vy += bottomTendencyForce;
}
fruit.x += fruit.vx;
fruit.y += fruit.vy;
var movementMagnitude = Math.sqrt(fruit.vx * fruit.vx + fruit.vy * fruit.vy);
var velocityChange = Math.abs(fruit.vx - fruit.lastVx) + Math.abs(fruit.vy - fruit.lastVy);
var isFullyStabilized = self.stabilizeFruit(fruit, movementMagnitude, velocityChange);
if (!isFullyStabilized) {
if (movementMagnitude > 0.5 || velocityChange > 0.3) {
var rotationFactor = 0.015 / (1 + (fruitLevel - 1) * 0.12);
var targetAngularVelocity = fruit.vx * rotationFactor;
fruit.angularVelocity = fruit.angularVelocity * 0.8 + targetAngularVelocity * 0.2;
} else {
fruit.angularVelocity *= 0.8;
}
fruit.rotation += fruit.angularVelocity;
var frictionModifier = 0.99 + (fruitLevel - 1) * 0.003;
fruit.vx *= fruit.friction * frictionModifier;
fruit.vy *= fruit.friction * frictionModifier * 0.99;
var horizontalStopThreshold = Math.max(0.05, 0.1 - (fruitLevel - 1) * 0.005);
var verticalStopThreshold = Math.max(0.05, 0.1 - (fruitLevel - 1) * 0.005);
if (Math.abs(fruit.vx) < horizontalStopThreshold) {
fruit.vx = 0;
}
if (Math.abs(fruit.vy) < verticalStopThreshold && fruit.y > gameHeight - 300) {
fruit.vy = 0;
}
self.handleRotationDamping(fruit);
}
};
self.handleRotationDamping = function (fruit) {
var movementMagnitude = Math.sqrt(fruit.vx * fruit.vx + fruit.vy * fruit.vy);
var rotationDampFactor = 0.9;
if (movementMagnitude < 0.05) {
rotationDampFactor = 0.6;
} else if (movementMagnitude < 0.3) {
rotationDampFactor = 0.7;
} else if (movementMagnitude < 0.5) {
rotationDampFactor = 0.8;
} else if (movementMagnitude < 0.8) {
rotationDampFactor = 0.85;
}
fruit.angularVelocity *= rotationDampFactor;
if (movementMagnitude > 0.3) {
var targetDirection = fruit.vx > 0 ? 1 : -1;
var currentDirection = fruit.angularVelocity > 0 ? 1 : -1;
if (targetDirection !== currentDirection && Math.abs(fruit.vx) > 1.0) {
fruit.angularVelocity *= 0.7;
}
}
if (Math.abs(fruit.angularVelocity) < 0.008) {
fruit.rotationRestCounter++;
if (fruit.rotationRestCounter > 6) {
fruit.angularVelocity = 0;
}
} else {
fruit.rotationRestCounter = 0;
}
fruit.angularVelocity = Math.min(Math.max(fruit.angularVelocity, -fruit.maxAngularVelocity * 1.2), fruit.maxAngularVelocity * 1.2);
};
return self;
});
var SpatialGrid = Container.expand(function (cellSize) {
var self = Container.call(this);
self.cellSize = cellSize || 200;
self.grid = {};
self.lastRebuildTime = Date.now();
self.rebuildInterval = 60000;
self.insertObject = function (obj) {
if (!obj || !obj.x || !obj.y || !obj.width || !obj.height || obj.merging || obj.isStatic) {
return;
}
obj._currentCells = obj._currentCells || [];
var cells = self.getCellsForObject(obj);
obj._currentCells = cells.slice();
for (var i = 0; i < cells.length; i++) {
var cellKey = cells[i];
if (!self.grid[cellKey]) {
self.grid[cellKey] = [];
}
if (self.grid[cellKey].indexOf(obj) === -1) {
self.grid[cellKey].push(obj);
}
}
};
self.removeObject = function (obj) {
if (!obj || !obj.x || !obj.y || !obj.width || !obj.height) {
return;
}
var cells = obj._currentCells || self.getCellsForObject(obj);
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);
}
if (self.grid[cellKey].length === 0) {
delete self.grid[cellKey];
}
}
}
obj._currentCells = [];
};
self.getCellsForObject = function (obj) {
if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number') {
return [];
}
var cells = [];
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);
}
}
return cells;
};
self.updateObject = function (obj) {
if (!obj || !obj.x || !obj.y || !obj.width || !obj.height) {
return;
}
var newCells = self.getCellsForObject(obj);
var oldCells = obj._currentCells || [];
var cellsChanged = false;
if (oldCells.length !== newCells.length) {
cellsChanged = true;
} else {
for (var i = 0; i < newCells.length; i++) {
if (oldCells.indexOf(newCells[i]) === -1) {
cellsChanged = true;
break;
}
}
}
if (cellsChanged) {
self.removeObject(obj);
self.insertObject(obj);
}
};
self.getPotentialCollisions = function (obj) {
var candidates = [];
var cells = self.getCellsForObject(obj);
var addedObjects = {};
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];
if (otherObj && otherObj !== obj && !addedObjects[otherObj.id]) {
candidates.push(otherObj);
addedObjects[otherObj.id] = true;
}
}
}
}
return candidates;
};
self.clear = function () {
self.grid = {};
self.lastRebuildTime = Date.now();
};
self.rebuildGrid = function (allObjects) {
self.grid = {};
self.lastRebuildTime = Date.now();
if (Array.isArray(allObjects)) {
for (var i = 0; i < allObjects.length; i++) {
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(100);
self.activeDots = [];
self.dots = [];
self.dotSpacing = 10;
self.dotSize = 15;
self.maxDots = 100;
self.createDots = function () {
self.clearDots();
self.dotPool.initialize(self.maxDots);
};
self.clearDots = function () {
for (var i = 0; i < self.activeDots.length; i++) {
if (self.activeDots[i]) {
self.removeChild(self.activeDots[i]);
self.dotPool.release(self.activeDots[i]);
}
}
self.activeDots = [];
};
self.updateTrajectory = function (startX, startY) {
if (!activeFruit) {
return;
}
self.clearDots();
var dotY = startY;
var dotSpacing = 25;
var dotCount = 0;
var hitDetected = false;
while (dotCount < self.maxDots && !hitDetected) {
var dot = self.dotPool.get();
self.addChild(dot);
self.activeDots.push(dot);
dot.x = startX;
dot.y = dotY;
dot.visible = true;
dot.alpha = 1.0;
dotCount++;
dotY += dotSpacing;
var floorCollisionY = gameFloor.y - gameFloor.height / 2 - activeFruit.height / 2;
if (dotY > floorCollisionY) {
if (dotCount > 0) {
self.activeDots[dotCount - 1].y = floorCollisionY;
}
hitDetected = true;
break;
}
var potentialHits = spatialGrid.getPotentialCollisions({
x: startX,
y: dotY,
width: activeFruit.width,
height: activeFruit.height,
id: 'trajectory_check'
});
for (var j = 0; j < potentialHits.length; j++) {
var fruit = potentialHits[j];
if (fruit && fruit !== activeFruit && !fruit.merging && fruit.width && fruit.height) {
if (self.wouldIntersectFruit(fruit.x, fruit.y, startX, dotY, activeFruit, fruit)) {
if (dotCount > 0) {
var dx = fruit.x - startX;
var dy = fruit.y - dotY;
var dist = Math.sqrt(dx * dx + dy * dy);
var overlap = activeFruit.width / 2 + fruit.width / 2 - dist;
if (dist > 0) {
self.activeDots[dotCount - 1].y = dotY - dy / dist * overlap;
}
}
hitDetected = true;
break;
}
}
}
}
};
self.wouldIntersectFruit = function (fruitX, fruitY, dropX, dropY, activeFruitObj, targetFruitObj) {
var dx = fruitX - dropX;
var dy = fruitY - dropY;
var distanceSquared = dx * dx + dy * dy;
var activeFruitLevel = getFruitLevel(activeFruitObj);
var targetFruitLevel = getFruitLevel(targetFruitObj);
var activeFruitRadius = activeFruitObj.width / 2;
var targetFruitRadius = targetFruitObj.width / 2;
var hitboxReduction = 0;
if (activeFruitLevel > 5) {
hitboxReduction += (activeFruitLevel - 5) * 3;
}
if (targetFruitLevel > 5) {
hitboxReduction += (targetFruitLevel - 5) * 3;
}
var combinedRadii = activeFruitRadius + targetFruitRadius - hitboxReduction;
var minDistanceSquared = combinedRadii * combinedRadii;
if (distanceSquared < minDistanceSquared) {
return true;
}
return false;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xf6e58d
});
/****
* Game Code
****/
var gameOverLine;
var pineapple;
var pineappleActive = false;
var pineapplePushCount = 0;
var readyToReleaseCharged = false;
var trajectoryLine;
var chargedFruitIconScale = 0.3;
var isClickable = true;
var evolutionLine;
var fireContainer;
var activeFireElements = [];
var BASE_FIRES = 3;
var FRUITS_PER_ADDITIONAL_FIRE = 3;
var FIRE_Y_START_OFFSET = 50;
var FIRE_Y_STACK_OFFSET = 100;
var FIRE_X_SPREAD = 500;
var MAX_FIRES = 15;
var fruitLevels = {
'CHERRY': 1,
'GRAPE': 2,
'APPLE': 3,
'ORANGE': 4,
'WATERMELON': 5,
'PINEAPPLE': 6,
'MELON': 7,
'PEACH': 8,
'COCONUT': 9,
'DURIAN': 10
};
function getFruitLevel(fruit) {
if (!fruit || !fruit.type || !fruit.type.id) {
return 10;
}
return fruitLevels[fruit.type.id.toUpperCase()] || 10;
}
function releasePineappleOnMerge() {
mergeCounter++;
pushPineapple();
if (mergeCounter >= 15 && !pineappleActive && pineapple) {
pineappleActive = true;
pineapple.isStatic = false;
applyDropPhysics(pineapple, 2.5);
fruits.push(pineapple);
if (spatialGrid) {
spatialGrid.insertObject(pineapple);
}
LK.setTimeout(function () {
if (pineapple && fruits.includes(pineapple)) {
pineapple.immuneToGameOver = false;
}
}, 2000);
setupPineapple();
mergeCounter = 0;
}
}
var FruitTypes = {
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'
},
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
}
};
var gameWidth = 2048;
var gameHeight = 2732;
var fruits = [];
var nextFruitType = null;
var activeFruit = null;
var wallLeft, wallRight, gameFloor;
var dropPointY = 200;
var gameOver = false;
var scoreText;
var isDragging = false;
var chargedBallUI = null;
var chargeCounter = 0;
var mergeCounter = 0;
var lastDroppedFruit = null;
var lastDroppedHasMerged = false;
var spatialGrid = null;
function setupBoundaries() {
wallLeft = game.addChild(LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
}));
wallLeft.x = 0;
wallLeft.y = gameHeight / 2;
wallLeft.alpha = 0;
wallRight = game.addChild(LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
}));
wallRight.x = gameWidth;
wallRight.y = gameHeight / 2;
wallRight.alpha = 0;
gameFloor = game.addChild(LK.getAsset('floor', {
anchorX: 0.5,
anchorY: 0.5
}));
gameFloor.x = gameWidth / 2;
gameFloor.y = gameHeight;
gameFloor.alpha = 0;
gameOverLine = game.addChild(new Line());
gameOverLine.x = gameWidth / 2;
gameOverLine.y = 550;
gameOverLine.scaleX = 1;
gameOverLine.scaleY = 0.2;
gameOverLine.alpha = 1;
}
function createNextFruit() {
var fruitProbability = Math.random();
var fruitType = fruitProbability < 0.6 ? FruitTypes.CHERRY : FruitTypes.GRAPE;
nextFruitType = fruitType;
activeFruit = new Fruit(nextFruitType);
activeFruit.x = lastDroppedFruit && lastDroppedFruit.x ? lastDroppedFruit.x : gameWidth / 2;
activeFruit.y = dropPointY + 200;
activeFruit.isStatic = true;
game.addChild(activeFruit);
if (trajectoryLine) {
trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y);
}
}
function dropFruit() {
if (gameOver || !activeFruit || !isClickable) {
return;
}
isClickable = false;
LK.setTimeout(function () {
isClickable = true;
}, 300);
activeFruit.isStatic = false;
applyDropPhysics(activeFruit, 3.5);
fruits.push(activeFruit);
spatialGrid.insertObject(activeFruit);
lastDroppedFruit = activeFruit;
lastDroppedHasMerged = false;
chargeCounter++;
updateChargedBallDisplay();
if (chargeCounter >= 15 && !readyToReleaseCharged) {
releaseChargedBalls();
}
activeFruit.mergeGracePeriodActive = true;
LK.setTimeout(function () {
if (activeFruit && fruits.includes(activeFruit)) {
activeFruit.mergeGracePeriodActive = false;
}
}, 2000);
if (trajectoryLine && trajectoryLine.dots && trajectoryLine.dots.length) {
for (var i = 0; i < trajectoryLine.dots.length; i++) {
trajectoryLine.dots[i].visible = false;
}
}
for (var i = 0; i < fruits.length; i++) {
if (fruits[i] && fruits[i].fromChargedRelease) {
fruits[i].fromChargedRelease = false;
}
}
LK.getSound('drop').play();
if (readyToReleaseCharged && chargeCounter >= 15) {
LK.getSound('pickleRick').play();
var orange = new Fruit(FruitTypes.ORANGE);
var minX = wallLeft.x + wallLeft.width / 2 + orange.width / 2 + 50;
var maxX = wallRight.x - wallRight.width / 2 - orange.width / 2 - 50;
orange.x = minX + Math.random() * (maxX - minX);
orange.y = -orange.height;
orange.isStatic = false;
var forceMultiplier = 3.5 + (Math.random() * 1 - 0.5);
applyDropPhysics(orange, forceMultiplier);
orange.fromChargedRelease = true;
game.addChild(orange);
fruits.push(orange);
spatialGrid.insertObject(orange);
chargeCounter = 0;
resetChargedBalls();
readyToReleaseCharged = false;
}
activeFruit = null;
createNextFruit();
}
function applyDropPhysics(fruit, forceMultiplier) {
var fruitLevel = getFruitLevel(fruit);
forceMultiplier *= 1.4;
var levelAdjustedForce = forceMultiplier * (1 + (fruitLevel - 1) * 0.05);
var mobilityReduction = Math.max(0, (fruitLevel - 2) * 0.03);
levelAdjustedForce *= 1 - mobilityReduction;
var angle = (Math.random() * 20 - 10) * (Math.PI / 180);
fruit.vx = Math.sin(angle) * levelAdjustedForce;
fruit.vy = Math.abs(Math.cos(angle) * levelAdjustedForce) * 1.5;
fruit.gravity = 13.0 * (1 + (fruitLevel - 1) * 0.2);
if (fruitLevel >= 5) {
fruit.gravity *= 1 + (fruitLevel - 5) * 0.03;
}
fruit.safetyPeriod = false;
fruit.immuneToGameOver = true;
LK.setTimeout(function () {
if (fruit && fruits.includes(fruit)) {
fruit.immuneToGameOver = false;
}
}, 1000);
}
function updateScoreDisplay() {
scoreText.setText(LK.getScore());
}
function setupUI() {
scoreText = new Text2("0", {
size: 120,
fill: 0x000000
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
scoreText.y = 400;
setupChargedBallDisplay();
}
function setupChargedBallDisplay() {
chargedBallUI = new ChargedBallUI();
chargedBallUI.initialize();
game.addChild(chargedBallUI);
}
function updateChargedBallDisplay() {
chargedBallUI && chargedBallUI.updateChargeDisplay(chargeCounter);
}
function releaseChargedBalls() {
readyToReleaseCharged = true;
chargedBallUI && chargedBallUI.setReadyState(true);
}
function resetChargedBalls() {
chargedBallUI && chargedBallUI.reset();
}
function checkFruitCollisions() {
for (var k = 0; k < fruits.length; k++) {
if (fruits[k]) {
fruits[k].neighboringFruits = 0;
fruits[k].neighborContacts = fruits[k].neighborContacts || [];
fruits[k].neighborContacts = [];
}
}
outerLoop: for (var i = fruits.length - 1; i >= 0; i--) {
var fruit1 = fruits[i];
if (!fruit1 || fruit1 === activeFruit || fruit1.merging || fruit1.isStatic) {
continue;
}
var candidates = spatialGrid.getPotentialCollisions(fruit1);
for (var j = 0; j < candidates.length; j++) {
var fruit2 = candidates[j];
if (!fruit2 || fruit2 === activeFruit || fruit2.merging || fruit2.isStatic || fruit1 === fruit2) {
continue;
}
if (fruits.indexOf(fruit2) === -1) {
continue;
}
var dx = fruit2.x - fruit1.x;
var dy = fruit2.y - fruit1.y;
var distanceSquared = dx * dx + dy * dy;
var distance = Math.sqrt(distanceSquared);
var fruit1HalfWidth = fruit1.width / 2;
var fruit1HalfHeight = fruit1.height / 2;
var fruit2HalfWidth = fruit2.width / 2;
var fruit2HalfHeight = fruit2.height / 2;
var absDistanceX = Math.abs(dx);
var absDistanceY = Math.abs(dy);
var level1 = getFruitLevel(fruit1);
var level2 = getFruitLevel(fruit2);
var hitboxReduction = 0;
if (level1 > 5) {
hitboxReduction += (level1 - 5) * 3;
}
if (level2 > 5) {
hitboxReduction += (level2 - 5) * 3;
}
var combinedHalfWidths = fruit1HalfWidth + fruit2HalfWidth - hitboxReduction / 2;
var combinedHalfHeights = fruit1HalfHeight + fruit2HalfHeight - hitboxReduction / 2;
var colliding = absDistanceX < combinedHalfWidths && absDistanceY < combinedHalfHeights;
if (colliding) {
if (fruit1.type === fruit2.type) {
fruit1.merge(fruit2);
continue outerLoop;
} else {
if (distance === 0) {
distance = 0.1;
dx = distance;
dy = 0;
}
var overlap = combinedHalfWidths - absDistanceX;
if (absDistanceY < combinedHalfHeights && absDistanceX < combinedHalfWidths) {
overlap = Math.min(combinedHalfWidths - absDistanceX, combinedHalfHeights - absDistanceY);
}
var normalizeX = dx / distance;
var normalizeY = dy / distance;
var moveX = overlap / 2 * normalizeX;
var moveY = overlap / 2 * normalizeY;
var separationFactor = 1.05;
fruit1.x -= moveX * separationFactor;
fruit1.y -= moveY * separationFactor;
fruit2.x += moveX * separationFactor;
fruit2.y += moveY * separationFactor;
var rvX = fruit2.vx - fruit1.vx;
var rvY = fruit2.vy - fruit1.vy;
var contactVelocity = rvX * normalizeX + rvY * normalizeY;
if (contactVelocity < 0) {
var collisionElasticity = Math.max(fruit1.elasticity, fruit2.elasticity);
var impulse = -(1 + collisionElasticity) * contactVelocity;
var level1 = getFruitLevel(fruit1);
var level2 = getFruitLevel(fruit2);
var mass1 = Math.pow(level1, 2.0) * Math.pow(fruit1.type.size, 1.5);
var mass2 = Math.pow(level2, 2.0) * Math.pow(fruit2.type.size, 1.5);
var totalMass = mass1 + mass2;
var impulseRatio1 = totalMass > 0 ? mass2 / totalMass : 0.5;
var impulseRatio2 = totalMass > 0 ? mass1 / totalMass : 0.5;
var levelDifference = Math.abs(level1 - level2);
var levelImpactMultiplier = 1 + levelDifference * 0.03;
var maxImpulseRatio = 1.7;
if (level1 > level2) {
impulseRatio2 = Math.min(impulseRatio2 * levelImpactMultiplier * 1.3, maxImpulseRatio * impulseRatio2);
} else if (level2 > level1) {
impulseRatio1 = Math.min(impulseRatio1 * levelImpactMultiplier * 1.3, maxImpulseRatio * impulseRatio1);
}
var velocityMagnitude = Math.sqrt(rvX * rvX + rvY * rvY);
var velocityDampening = 1.0;
if (velocityMagnitude > 5) {
velocityDampening = 0.5 + 0.5 * (5 / velocityMagnitude);
}
var impulse1 = impulse * impulseRatio1 * velocityDampening;
var impulse2 = impulse * impulseRatio2 * velocityDampening;
var sizeDifference = Math.abs(fruit1.type.size - fruit2.type.size) / Math.max(fruit1.type.size, fruit2.type.size, 1);
if (fruit1.type.size < fruit2.type.size) {
impulse1 *= 1 + sizeDifference * 2.5;
fruit1._lastBounceTime = Date.now();
} else if (fruit2.type.size < fruit1.type.size) {
impulse2 *= 1 + sizeDifference * 2.5;
fruit2._lastBounceTime = Date.now();
}
fruit1.vx -= impulse1 * normalizeX;
fruit1.vy -= impulse1 * normalizeY;
fruit2.vx += impulse2 * normalizeX;
fruit2.vy += impulse2 * normalizeY;
var tangentX = -normalizeY;
var tangentY = normalizeX;
var tangentVelocity = rvX * tangentX + rvY * tangentY;
var frictionImpulse = -tangentVelocity * 0.1;
fruit1.vx -= frictionImpulse * tangentX * impulseRatio1;
fruit1.vy -= frictionImpulse * tangentY * impulseRatio1;
fruit2.vx += frictionImpulse * tangentX * impulseRatio2;
fruit2.vy += frictionImpulse * tangentY * impulseRatio2;
var rotationTransferFactor = 0.01;
var tangentialComponent = rvX * tangentX + rvY * tangentY;
var inertia1 = mass1 * Math.pow(fruit1.width / 2, 2);
var inertia2 = mass2 * Math.pow(fruit2.width / 2, 2);
var angularImpulse = tangentialComponent * rotationTransferFactor;
fruit1.angularVelocity += inertia2 > 0 ? angularImpulse * (inertia1 / (inertia1 + inertia2)) : angularImpulse * 0.5;
fruit2.angularVelocity -= inertia1 > 0 ? angularImpulse * (inertia2 / (inertia1 + inertia2)) : angularImpulse * 0.5;
fruit1.angularVelocity *= 0.95;
fruit2.angularVelocity *= 0.95;
fruit1.angularVelocity = Math.min(Math.max(fruit1.angularVelocity, -fruit1.maxAngularVelocity), fruit1.maxAngularVelocity);
fruit2.angularVelocity = Math.min(Math.max(fruit2.angularVelocity, -fruit2.maxAngularVelocity), fruit2.maxAngularVelocity);
fruit1.neighboringFruits = (fruit1.neighboringFruits || 0) + 1;
fruit2.neighboringFruits = (fruit2.neighboringFruits || 0) + 1;
fruit1.neighborContacts = fruit1.neighborContacts || [];
fruit2.neighborContacts = fruit2.neighborContacts || [];
if (fruit1.neighborContacts.indexOf(fruit2.id) === -1) {
fruit1.neighborContacts.push(fruit2.id);
}
if (fruit2.neighborContacts.indexOf(fruit1.id) === -1) {
fruit2.neighborContacts.push(fruit1.id);
}
var currentTime = Date.now();
fruit1._collisionHistory = fruit1._collisionHistory || {};
fruit2._collisionHistory = fruit2._collisionHistory || {};
var recentCollision = false;
if (fruit1._collisionHistory[fruit2.id] && currentTime - fruit1._collisionHistory[fruit2.id] < 300) {
recentCollision = true;
}
fruit1._collisionHistory[fruit2.id] = currentTime;
fruit2._collisionHistory[fruit1.id] = currentTime;
if (recentCollision) {
fruit1.vx *= 0.7;
fruit1.vy *= 0.7;
fruit2.vx *= 0.7;
fruit2.vy *= 0.7;
fruit1.angularVelocity *= 0.6;
fruit2.angularVelocity *= 0.6;
}
if (fruit1.neighborContacts.length >= 3 || fruit2.neighborContacts.length >= 3) {
var stabilizeFactor = 0.6;
fruit1.vx *= stabilizeFactor;
fruit1.vy *= stabilizeFactor;
fruit2.vx *= stabilizeFactor;
fruit2.vy *= stabilizeFactor;
fruit1.angularVelocity *= 0.75;
fruit2.angularVelocity *= 0.75;
}
}
}
}
}
}
}
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) {
continue;
}
var fruitHalfHeight = fruit.height / 2;
var fruitHalfWidth = fruit.width / 2;
var fruitLevel = getFruitLevel(fruit);
var sizeReduction = 0;
if (fruitLevel > 5) {
sizeReduction = (fruitLevel - 5) * 3;
}
fruitHalfHeight = Math.max(10, fruitHalfHeight - sizeReduction / 2);
fruitHalfWidth = Math.max(10, fruitHalfWidth - sizeReduction / 2);
var cosAngle = Math.abs(Math.cos(fruit.rotation));
var sinAngle = Math.abs(Math.sin(fruit.rotation));
var effectiveHeight = fruitHalfHeight * cosAngle + fruitHalfWidth * sinAngle;
var fruitTopY = fruit.y - effectiveHeight;
var lineBottomY = gameOverLine.y + gameOverLine.height / 2;
if (fruitTopY <= lineBottomY) {
var effectiveWidth = fruitHalfWidth * cosAngle + fruitHalfHeight * sinAngle;
var fruitLeftX = fruit.x - effectiveWidth;
var fruitRightX = fruit.x + effectiveWidth;
var lineLeftX = gameOverLine.x - gameOverLine.width * gameOverLine.scaleX / 2;
var lineRightX = gameOverLine.x + gameOverLine.width * gameOverLine.scaleX / 2;
var horizontalOverlap = !(fruitRightX < lineLeftX || fruitLeftX > lineRightX);
if (horizontalOverlap) {
if (fruit.immuneToGameOver) {
continue;
}
var stableOrSlowing = Math.abs(fruit.vy) < 1.0 || fruit._boundaryContacts && (fruit._boundaryContacts.left || fruit._boundaryContacts.right || fruit._boundaryContacts.floor);
if (stableOrSlowing) {
gameOver = true;
LK.showGameOver();
return;
}
}
} else {
fruit.safetyPeriod = undefined;
}
}
}
function setupPineapple() {
pineapple = new Fruit(FruitTypes.PINEAPPLE);
pineapple.x = -pineapple.width / 2;
pineapple.y = 200;
pineapple.isStatic = true;
pineappleActive = false;
pineapplePushCount = 0;
game.addChild(pineapple);
}
function pushPineapple() {
if (!pineappleActive && pineapple) {
var step = mergeCounter;
var totalSteps = 15;
var percentage = Math.min(step / totalSteps, 1.0);
var startPos = -pineapple.width / 2;
var endPos = gameWidth * 0.16;
var newX = startPos + percentage * (endPos - startPos);
tween(pineapple, {
x: newX
}, {
duration: 300,
easing: tween.bounceOut
});
}
}
function initGame() {
LK.setScore(0);
gameOver = false;
fruits = [];
chargeCounter = 0;
if (chargedBallUI) {
chargedBallUI.destroy();
}
chargedBallUI = null;
readyToReleaseCharged = false;
lastScoreCheckForCoconut = 0;
lastDroppedFruit = null;
lastDroppedHasMerged = false;
mergeCounter = 0;
isClickable = true;
// Initialize fire background
if (fireContainer) {
// Clean up existing fire elements
for (var i = 0; i < activeFireElements.length; i++) {
if (activeFireElements[i]) {
activeFireElements[i].destroy();
}
}
fireContainer.destroy();
}
fireContainer = new Container();
game.addChildAt(fireContainer, 0); // Add at bottom layer
activeFireElements = [];
if (spatialGrid) {
spatialGrid.clear();
} else {
var avgFruitSize = 0;
var fruitCount = 0;
for (var fruitType in FruitTypes) {
if (FruitTypes.hasOwnProperty(fruitType)) {
avgFruitSize += FruitTypes[fruitType].size;
fruitCount++;
}
}
avgFruitSize = fruitCount > 0 ? avgFruitSize / fruitCount : 300;
var optimalCellSize = Math.ceil(avgFruitSize * 1.1);
spatialGrid = new SpatialGrid(optimalCellSize);
}
LK.playMusic('bgmusic');
if (wallLeft) {
wallLeft.destroy();
}
if (wallRight) {
wallRight.destroy();
}
if (gameFloor) {
gameFloor.destroy();
}
if (gameOverLine) {
gameOverLine.destroy();
}
if (pineapple) {
pineapple.destroy();
}
if (trajectoryLine) {
trajectoryLine.destroy();
}
if (evolutionLine) {
evolutionLine.destroy();
}
setupBoundaries();
setupUI();
setupPineapple();
updateFireBackground(); // Initial setup of fire elements
trajectoryLine = game.addChild(new TrajectoryLine());
trajectoryLine.createDots();
evolutionLine = game.addChild(new EvolutionLine());
evolutionLine.initialize();
updateScoreDisplay();
activeFruit = null;
createNextFruit();
resetChargedBalls();
updateFireBackground(); // Initial setup of fire elements
}
function spawnCoconut() {
var coconut = new Fruit(FruitTypes.COCONUT);
var minX = wallLeft.x + wallLeft.width / 2 + coconut.width / 2 + 50;
var maxX = wallRight.x - wallRight.width / 2 - coconut.width / 2 - 50;
coconut.x = minX + Math.random() * (maxX - minX);
coconut.y = gameHeight + coconut.height / 2;
coconut.isStatic = true;
game.addChild(coconut);
fruits.push(coconut);
coconut.safetyPeriod = false;
coconut.immuneToGameOver = true;
var targetY = gameHeight - gameFloor.height / 2 - coconut.height / 2 - 10;
tween(coconut, {
y: targetY
}, {
duration: 1200,
easing: tween.easeIn,
onFinish: function onFinish() {
if (!coconut || !fruits.includes(coconut)) {
return;
}
coconut.isStatic = false;
coconut.vy = -2;
coconut.vx = (Math.random() * 2 - 1) * 1.5;
spatialGrid.insertObject(coconut);
LK.setTimeout(function () {
if (coconut && fruits.includes(coconut)) {
coconut.immuneToGameOver = false;
}
}, 1000);
}
});
}
var lastScoreCheckForCoconut = 0;
game.down = function (x, y) {
if (activeFruit && !gameOver) {
isDragging = true;
game.move(x, y);
}
};
game.move = function (x, y) {
if (isDragging && activeFruit && !gameOver) {
var fruitRadius = activeFruit.width / 2;
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));
activeFruit.y = dropPointY + 200;
if (trajectoryLine) {
trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y);
}
}
};
game.up = function () {
if (isDragging && activeFruit && isClickable && !gameOver) {
dropFruit();
}
isDragging = false;
};
function updatePhysics() {
if (spatialGrid && Date.now() - spatialGrid.lastRebuildTime > spatialGrid.rebuildInterval) {
spatialGrid.rebuildGrid(fruits);
}
for (var i = fruits.length - 1; i >= 0; i--) {
var fruit = fruits[i];
if (!fruit || fruit.isStatic || fruit.merging) {
continue;
}
fruit.updatePhysics();
var walls = {
left: wallLeft,
right: wallRight
};
fruit.checkBoundaries(walls, gameFloor);
}
checkFruitCollisions();
for (var i = 0; i < fruits.length; i++) {
var fruit = fruits[i];
if (fruit && !fruit.isStatic && !fruit.merging) {
if (fruit.surroundedFrames === undefined) {
fruit.surroundedFrames = 0;
}
var boundaryContact = fruit.wallContactFrames > 0;
var floorProximity = fruit.y + fruit.height / 2 >= gameFloor.y - gameFloor.height / 2 - 10;
var hasMultipleNeighbors = fruit.neighborContacts && fruit.neighborContacts.length >= 2;
var isSlowMoving = Math.abs(fruit.vx) < 0.3 && Math.abs(fruit.vy) < 0.3;
var hasGapBelow = false;
if (floorProximity || hasMultipleNeighbors) {
var potentialNeighbors = spatialGrid.getPotentialCollisions({
x: fruit.x,
y: fruit.y + fruit.height / 2 + 20,
width: fruit.width * 0.6,
height: 20,
id: 'gap_check_' + fruit.id
});
hasGapBelow = potentialNeighbors.length === 0 && !floorProximity;
if (hasGapBelow && isSlowMoving) {
var leftCount = 0,
rightCount = 0;
for (var j = 0; j < fruit.neighborContacts.length; j++) {
var neighborId = fruit.neighborContacts[j];
for (var k = 0; k < fruits.length; k++) {
if (fruits[k] && fruits[k].id === neighborId) {
if (fruits[k].x < fruit.x) {
leftCount++;
} else if (fruits[k].x > fruit.x) {
rightCount++;
}
break;
}
}
}
if (leftCount < rightCount) {
fruit.vx -= 0.05;
} else if (rightCount < leftCount) {
fruit.vx += 0.05;
} else {
fruit.vx += Math.random() * 0.1 - 0.05;
}
}
}
var stabilizationScenario = boundaryContact ? "boundary" : floorProximity ? "floor" : hasMultipleNeighbors ? "surrounded" : "free";
switch (stabilizationScenario) {
case "boundary":
fruit.surroundedFrames = 0;
break;
case "floor":
fruit.surroundedFrames = 0;
break;
case "surrounded":
fruit.surroundedFrames++;
var stabilizationStrength = Math.min(0.92, 0.75 + fruit.surroundedFrames * 0.015);
if (fruit.neighborContacts.length >= 3) {
stabilizationStrength = Math.min(0.95, stabilizationStrength + 0.03);
}
fruit.vx *= stabilizationStrength;
fruit.vy *= stabilizationStrength;
fruit.angularVelocity *= stabilizationStrength;
if (fruit.surroundedFrames > 10 && isSlowMoving || fruit.surroundedFrames > 18) {
if (Math.abs(fruit.vx) < 0.05) {
fruit.vx = 0;
}
if (Math.abs(fruit.vy) < 0.05) {
fruit.vy = 0;
}
if (Math.abs(fruit.angularVelocity) < 0.005) {
fruit.angularVelocity = 0;
}
}
break;
case "free":
fruit.surroundedFrames = 0;
break;
}
spatialGrid.updateObject(fruit);
}
}
}
game.update = function () {
if (gameOver) {
return;
}
var currentScore = LK.getScore();
if (currentScore >= lastScoreCheckForCoconut + 1000) {
lastScoreCheckForCoconut = Math.floor(currentScore / 1000) * 1000;
spawnCoconut();
} else {
if (currentScore > lastScoreCheckForCoconut) {
lastScoreCheckForCoconut = Math.floor(currentScore / 1000) * 1000;
}
}
updatePhysics();
checkGameOver();
// Update fire background
updateFireBackground();
// Animate individual fire elements
for (var i = 0; i < activeFireElements.length; i++) {
if (activeFireElements[i]) {
activeFireElements[i].update();
}
}
};
initGame();
function updateFireBackground() {
// Track unique fruit types on the board
var uniqueFruitTypes = {};
for (var i = 0; i < fruits.length; i++) {
var fruit = fruits[i];
if (fruit && !fruit.isStatic && !fruit.merging && fruit.type && fruit.type.id) {
uniqueFruitTypes[fruit.type.id] = true;
}
}
// Count the number of unique fruit types
var uniqueTypeCount = Object.keys(uniqueFruitTypes).length;
// Calculate target number of fire elements: BASE_FIRES + one for each unique fruit type
var targetFireCount = BASE_FIRES + uniqueTypeCount;
targetFireCount = Math.min(targetFireCount, MAX_FIRES);
// Create or remove fires to match target count
if (targetFireCount > activeFireElements.length) {
// Need to add more fires
var newFiresCount = targetFireCount - activeFireElements.length;
for (var i = 0; i < newFiresCount; i++) {
// Create fire at appropriate position
createFireElement(activeFireElements.length);
}
} else if (targetFireCount < activeFireElements.length) {
// Need to remove excess fires
var fireToRemoveCount = activeFireElements.length - targetFireCount;
for (var i = 0; i < fireToRemoveCount; i++) {
var fire = activeFireElements.pop(); // Remove from end (highest/last added fires)
if (fire) {
fire.destroy(); // Ensure destroy() is called to clean up resources
fireContainer.removeChild(fire);
}
}
}
}
// Helper function to create a single fire element at the specified index
function createFireElement(index) {
// Calculate y position (higher fires appear behind/further back)
var yPos = gameHeight + FIRE_Y_START_OFFSET - index * FIRE_Y_STACK_OFFSET;
// Randomize x position around center of screen
var xPos = gameWidth / 2 + (Math.random() * FIRE_X_SPREAD - FIRE_X_SPREAD / 2);
// Create new fire element
var newFire = new FireElement(xPos, yPos);
// Alternate x-axis flip for all fires including the first three
if (index % 2 === 1) {
newFire.fireAsset.scaleX = -1; // Flip every other fire
}
// Add at index 0 to render behind existing fires
fireContainer.addChildAt(newFire, 0);
activeFireElements.push(newFire);
}
function removeFruitFromGame(fruit) {
var index = fruits.indexOf(fruit);
if (index !== -1) {
fruits.splice(index, 1);
}
spatialGrid.removeObject(fruit);
fruit.destroy();
}