/**** * 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();
}