Code edit (1 edits merged)
Please save this source code
User prompt
increase the fruits falling speed
Code edit (1 edits merged)
Please save this source code
User prompt
make smaller fruits vibrate even more off larger fruits, but remove the reverberation that happens when they keep bouncing between multiple fruits ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
make smaller fruits bounce a bit more off larger fruits
User prompt
ok, now, can you help me add an evolution line at the top of the screen? SHow all 10 fruits on a grid, a single row containing all 10 fruits, from level 1 on the left to level 10 on the right. this is an UI element, that will remain on the screen to show players the evolution of the fruits. all icons should maintain their aspect raio, but the width and height can be up to 100 pixels
User prompt
fix the game over line as it no longer records fruits that should trigger a game over state. only fruits that have already touches the floor, walls or other fruits count
Code edit (1 edits merged)
Please save this source code
User prompt
increase the size of the score
User prompt
move the charging fruits text UI to the right by 100 pixels
User prompt
make fruits fall even faster
User prompt
make fruits fall even faster
User prompt
larger fruits hitbox is still weird sometimes, pushing away fruits before they actually hit the asset sie, as if the hitbox is larger than the asset size
User prompt
now fruits drop too slow when dropped, like the air is water, the density is too large, make them drop faster
User prompt
something still makes small fruits jumps like popcorn. i think it ahs soemthing to do with when their force amplifies when beeeing bounced between multiple fruits. can you fix this, so they dont jump like popcorn?
Code edit (1 edits merged)
Please save this source code
User prompt
it seems that niow larger fruits tend to bounce around smaller fruits way too much, decrease that force so it doesn't push them so violently far away
User prompt
I'm notsure using gravity is necesarely the best approach for this problem, bin the sense gravity should be the same for all fruits, BUT, larger fruits should tend to go more to the bottom of the screen, since they are heavier
User prompt
increase the effect gravity has on larger fruits. just that effect, nothing else
User prompt
Add gravity increase to larger fruits for more realistic physics. increase the gravity effect even more
User prompt
the higher the fruit's level is, the larger the gravity impact it, so larger fruits tend to go more to the bottom and also move less, or be heavier to be moved
User prompt
fruits behave a bit weird, they tend to snap back to their upward position so they never rest diagonally... and they also leave large gaps between the fruits, when fruits should be rolling into those spaces. idk, the physicsl still feel a bit off and kind of rigid
User prompt
gravity still doesnt work, released fruits just remain stuck in place
User prompt
gravity still doesnt work, released fruits just remain stuck in place
User prompt
gravity still doesnt work, released fruits just remain there
/**** * 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 = 9; self.currentCharge = 0; self.isReadyToRelease = false; self.countdownText = null; self.pulseAnimationActive = false; self.initialize = function () { self.countdownText = new Text2(self.chargeNeededForRelease.toString(), { size: 100, fill: 0x000000 }); self.countdownText.anchor.set(0.5, 0.5); self.countdownText.x = gameWidth / 2 + 770; self.addChild(self.countdownText); self.y = 120; }; self.updateChargeDisplay = function (chargeCount) { self.currentCharge = chargeCount; var remainingCount = Math.max(0, self.chargeNeededForRelease - self.currentCharge); self.countdownText.setText(remainingCount.toString()); var textColor = remainingCount <= 3 ? 0xFF0000 : remainingCount <= 6 ? 0xFFA500 : 0x000000; tween(self.countdownText, { tint: textColor }, { duration: 300, easing: tween.easeOut }); var baseSize = 1.0; var sizeMultiplier = baseSize + 0.2 * (self.chargeNeededForRelease - remainingCount); tween(self.countdownText, { scaleX: sizeMultiplier, scaleY: sizeMultiplier }, { duration: 300, easing: tween.easeOut }); if (remainingCount === 0 && !self.isReadyToRelease) { self.setReadyState(true); } }; self.setReadyState = function (isReady) { self.isReadyToRelease = isReady; if (isReady) { self.countdownText.setText("0"); tween(self.countdownText, { tint: 0xFF0000 }, { 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.countdownText, { scaleX: 1.3, scaleY: 1.3 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.isReadyToRelease) { self.pulseAnimationActive = false; return; } tween(self.countdownText, { 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; self.countdownText.setText(self.chargeNeededForRelease.toString()); tween(self.countdownText, { tint: 0x000000 }, { duration: 200, easing: tween.easeOut }); tween(self.countdownText, { scaleX: 1.0, scaleY: 1.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 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; // Track wall contact state before collision checks var wasInContact = fruit.wallContactFrames > 0; // Track which boundaries the fruit is touching fruit._boundaryContacts = fruit._boundaryContacts || { left: false, right: false, floor: false }; // Reset boundary contacts at start of each check fruit._boundaryContacts.left = false; fruit._boundaryContacts.right = false; fruit._boundaryContacts.floor = false; // Check each boundary collision self.checkLeftWallCollision(fruit, walls.left, effectiveWidth); self.checkRightWallCollision(fruit, walls.right, effectiveWidth); self.checkFloorCollision(fruit, floor, effectiveHeight); // Update wall contact frames counter based on contact with any boundary var isInContact = fruit._boundaryContacts.left || fruit._boundaryContacts.right || fruit._boundaryContacts.floor; if (isInContact) { fruit.wallContactFrames++; // Apply progressive friction based on contact duration var progressiveFriction = Math.min(0.85, 0.65 + fruit.wallContactFrames * 0.01); fruit.angularVelocity *= progressiveFriction; // Apply more aggressive stabilization for prolonged contact // But still let gravity work if not on floor if (fruit.wallContactFrames > 10) { fruit.vx *= 0.9; // Only dampen vertical velocity if on floor if (fruit._boundaryContacts.floor) { fruit.vy *= 0.9; } } } else { // Gradually decrease contact counter when not touching walls 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.6; var angularImpactMultiplier = 0.002 * (1 + (0.9 - fruit.elasticity) * 5); fruit.angularVelocity += fruit.vy * angularImpactMultiplier * 0.2; fruit.angularVelocity *= fruit.groundAngularFriction * 0.7; // Track that fruit is touching left wall fruit._boundaryContacts.left = true; // Progressive stabilization based on contact duration if (fruit.wallContactFrames > 2 && Math.abs(fruit.vx) < 1.0) { fruit.vx = 0; } } else if (fruit.x > leftWall.x + leftWall.width * 2) { // Only reset here if we're far from the wall 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.6; var angularImpactMultiplier = 0.002 * (1 + (0.9 - fruit.elasticity) * 5); fruit.angularVelocity -= fruit.vy * angularImpactMultiplier * 0.2; fruit.angularVelocity *= fruit.groundAngularFriction * 0.7; // Track that fruit is touching right wall fruit._boundaryContacts.right = true; // Progressive stabilization based on contact duration if (fruit.wallContactFrames > 2 && Math.abs(fruit.vx) < 1.0) { 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; // Bounce based on elasticity and size (larger fruits bounce less) var fruitLevel = getFruitLevel(fruit); var bounceDamping = 0.5 * (1 - (fruitLevel - 1) * 0.05); fruit.vy = -fruit.vy * fruit.elasticity * bounceDamping; // Track that fruit is touching floor fruit._boundaryContacts.floor = true; // Update angular velocity based on horizontal movement if (Math.abs(fruit.vx) > 0.5) { fruit.angularVelocity = fruit.vx * 0.01; } fruit.angularVelocity *= fruit.groundAngularFriction * 0.7; // Only stop vertical movement if the bounce is small enough var bounceMagnitude = Math.abs(fruit.vy); var restThreshold = fruit.wallContactFrames > 5 ? 1.5 : 2.5; if (bounceMagnitude < restThreshold) { fruit.vy = 0; fruit.vx *= 0.6; // More aggressive stabilization for fruits that have been on floor for a while if (fruit.wallContactFrames > 8) { fruit.vx *= 0.8; } } // Angular rest handling with adaptive threshold var angularRestThreshold = fruit.wallContactFrames > 5 ? 0.02 : 0.03; if (Math.abs(fruit.angularVelocity) < angularRestThreshold) { fruit.angularVelocity = 0; fruit.rotation = Math.round(fruit.rotation / (Math.PI / 2)) * (Math.PI / 2); } } }; 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 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; self.gravity = physics.gravity; // Make larger fruits have slightly less friction (slide/fall more easily) self.friction = physics.friction * (1 - (currentLevel - 1) * 0.01); self.rotationRestCounter = physics.rotationRestCounter; self.maxAngularVelocity = physics.maxAngularVelocity; self.isStatic = physics.isStatic; var currentLevel = getFruitLevel(self); // Make larger fruits significantly less bouncy (more affected by gravity) self.elasticity = 0.9 - (currentLevel - 1) * (0.4 / 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; // Execute spawn behavior if defined if (self.behavior && self.behavior.onSpawn) { self.behavior.onSpawn(self); } } else { console.log("Warning: Fruit type not available yet or missing required properties"); } // Method to handle physics update self.updatePhysics = function () { physics.apply(self); }; // Method to handle boundary collisions self.checkBoundaries = function (walls, floor) { collision.checkBoundaryCollisions(self, walls, floor); // Safety period check for game over conditions if (self.safetyPeriod === false && self.vy <= 0.1) { self.safetyPeriod = undefined; // Reset safety check state } }; // Merge method (delegates to merge component) 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) { // Special durian behavior LK.setScore(LK.getScore() + fruit1.type.points); updateScoreDisplay(); removeFruitFromGame(fruit1); removeFruitFromGame(fruit2); releasePineappleOnMerge(); return null; // No new fruit } } }; 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; }; // Centralized sound effect manager 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(); // Track merge for gameplay mechanics self.trackMerge(fruit1, fruit2); // Get the appropriate behavior handler for this fruit type var behaviorHandler = self.fruitBehavior.getMergeHandler(fruit1.type.id); behaviorHandler.onMerge(fruit1, fruit2, midX, midY); // Cleanup the original fruits 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; // Track stabilization metrics in a single place if (!fruit._stabilizeMetrics) { fruit._stabilizeMetrics = { consecutiveSmallMovements: 0, wallContactDuration: 0, surroundedDuration: 0, restingDuration: 0 }; } // Combine all stabilization factors if (movementMagnitude < 0.3 && Math.abs(fruit.angularVelocity) < 0.03) { fruit._stabilizeMetrics.consecutiveSmallMovements++; fruit._stabilizeMetrics.restingDuration++; } else { fruit._stabilizeMetrics.consecutiveSmallMovements = 0; fruit._stabilizeMetrics.restingDuration = Math.max(0, fruit._stabilizeMetrics.restingDuration - 1); } // Update wall contact tracking if (fruit.wallContactFrames > 0) { fruit._stabilizeMetrics.wallContactDuration++; } else { fruit._stabilizeMetrics.wallContactDuration = Math.max(0, fruit._stabilizeMetrics.wallContactDuration - 1); } // Update surrounded tracking if (fruit.surroundedFrames > 0) { fruit._stabilizeMetrics.surroundedDuration++; } else { fruit._stabilizeMetrics.surroundedDuration = Math.max(0, fruit._stabilizeMetrics.surroundedDuration - 1); } // Determine stabilization level based on combined factors var totalStabilizationScore = fruit._stabilizeMetrics.consecutiveSmallMovements * 1.0 + fruit._stabilizeMetrics.wallContactDuration * 0.8 + fruit._stabilizeMetrics.surroundedDuration * 1.2 + fruit._stabilizeMetrics.restingDuration * 0.5; // Make stabilization thresholds higher to prevent premature stabilization if (totalStabilizationScore > 20) { stabilizationLevel = 2; // Full stabilization } else if (totalStabilizationScore > 12) { stabilizationLevel = 1; // Partial stabilization } // Apply stabilization based on level if (stabilizationLevel > 0) { // Dampen movement increasingly with stabilization level var dampFactor = stabilizationLevel === 2 ? 0.5 : 0.8; fruit.vx *= dampFactor; // Don't dampen vertical velocity as aggressively to allow gravity to work fruit.vy *= stabilizationLevel === 2 ? 0.7 : 0.9; fruit.angularVelocity *= dampFactor; // For full stabilization, only stop horizontal movement completely // Let gravity continue to work vertically unless firmly on ground if (stabilizationLevel === 2 && movementMagnitude < 0.1) { fruit.vx = 0; // Only zero out vertical velocity if fruit is touching floor if (fruit._boundaryContacts && fruit._boundaryContacts.floor) { fruit.vy = 0; } fruit.angularVelocity = 0; fruit.rotation = Math.round(fruit.rotation / (Math.PI / 2)) * (Math.PI / 2); return true; } } return false; }; self.apply = function (fruit) { if (fruit.isStatic || fruit.merging) { return; } fruit.lastVx = fruit.vx; fruit.lastVy = fruit.vy; // Scale gravity based on fruit level/size var fruitLevel = getFruitLevel(fruit); var gravityMultiplier = 0.8 + fruitLevel * 0.2; // Always apply gravity regardless of stabilization state fruit.vy += fruit.gravity * gravityMultiplier; 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); // Unified stabilization check var isFullyStabilized = self.stabilizeFruit(fruit, movementMagnitude, velocityChange); // Skip further processing if fruit is fully stabilized if (!isFullyStabilized) { // Angular velocity adjustment based on movement if (movementMagnitude > 0.5 || velocityChange > 0.3) { var targetAngularVelocity = fruit.vx * 0.015; fruit.angularVelocity = fruit.angularVelocity * 0.8 + targetAngularVelocity * 0.2; } else { fruit.angularVelocity *= 0.8; } fruit.rotation += fruit.angularVelocity; // Apply friction but don't completely kill momentum too easily fruit.vx *= fruit.friction; fruit.vy *= fruit.friction; // Velocity thresholds for stopping - higher threshold for vertical to let gravity work if (Math.abs(fruit.vx) < 0.1) { fruit.vx = 0; } // Only stop vertical movement if very slow AND near the floor if (Math.abs(fruit.vy) < 0.1 && 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); // Progressive rotation damping based on movement var rotationDampFactor = 0.85; if (movementMagnitude < 0.05) { rotationDampFactor = 0.5; } else if (movementMagnitude < 0.3) { rotationDampFactor = 0.6; } else if (movementMagnitude < 0.5) { rotationDampFactor = 0.7; } else if (movementMagnitude < 0.8) { rotationDampFactor = 0.8; } fruit.angularVelocity *= rotationDampFactor; // Direction correction for rotation if (movementMagnitude > 0.2) { var targetDirection = fruit.vx > 0 ? 1 : -1; var currentDirection = fruit.angularVelocity > 0 ? 1 : -1; if (targetDirection !== currentDirection && Math.abs(fruit.vx) > 0.8) { fruit.angularVelocity *= 0.5; } } // Rotation rest handling if (Math.abs(fruit.angularVelocity) < 0.01) { fruit.rotationRestCounter++; if (fruit.rotationRestCounter > 5) { fruit.angularVelocity = 0; fruit.rotation = Math.round(fruit.rotation / (Math.PI / 2)) * (Math.PI / 2); } } else { fruit.rotationRestCounter = 0; } // Enforce angular velocity limits fruit.angularVelocity = Math.min(Math.max(fruit.angularVelocity, -fruit.maxAngularVelocity), fruit.maxAngularVelocity); }; 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; // Rebuild grid every minute to prevent memory leaks self.insertObject = function (obj) { if (!obj || !obj.x || !obj.y || !obj.width || !obj.height || obj.merging || obj.isStatic) { return; } // Store the current cells the object belongs to for efficient updates obj._currentCells = obj._currentCells || []; var cells = self.getCellsForObject(obj); // Store new cells for faster future updates obj._currentCells = cells.slice(); for (var i = 0; i < cells.length; i++) { var cellKey = cells[i]; if (!self.grid[cellKey]) { self.grid[cellKey] = []; } // Using a faster direct check instead of iterating the entire cell array 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; } // Use cached cells if available, otherwise calculate them 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); } // Clean up empty cells to prevent memory leaks if (self.grid[cellKey].length === 0) { delete self.grid[cellKey]; } } } // Clear the cached cells 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) { // Optimize by only updating if the object has actually moved between cells if (!obj || !obj.x || !obj.y || !obj.width || !obj.height) { return; } // Get new cells var newCells = self.getCellsForObject(obj); // Get old cells var oldCells = obj._currentCells || []; // Check if cells have changed var cellsChanged = false; if (oldCells.length !== newCells.length) { cellsChanged = true; } else { // Check if any cell is different for (var i = 0; i < newCells.length; i++) { if (oldCells.indexOf(newCells[i]) === -1) { cellsChanged = true; break; } } } // Only update if cells have changed to avoid unnecessary operations if (cellsChanged) { self.removeObject(obj); // Remove from old cells self.insertObject(obj); // Insert to new cells } }; 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]; // Additional check to ensure otherObj has an id 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) { // Clear the grid self.grid = {}; self.lastRebuildTime = Date.now(); // Re-insert all active objects 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 = []; // Initialize dots array 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 combinedRadii = activeFruitObj.width / 2 + targetFruitObj.width / 2; var minDistanceSquared = (combinedRadii - 5) * (combinedRadii - 5); 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 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 >= 10 && !pineappleActive && pineapple) { pineappleActive = true; pineapple.isStatic = false; applyDropPhysics(pineapple, 2.5); fruits.push(pineapple); // Add to spatial grid immediately after adding to fruits 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; wallRight = game.addChild(LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 })); wallRight.x = gameWidth; wallRight.y = gameHeight / 2; gameFloor = game.addChild(LK.getAsset('floor', { anchorX: 0.5, anchorY: 0.5 })); gameFloor.x = gameWidth / 2; gameFloor.y = gameHeight; 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; // Start slightly higher activeFruit.isStatic = true; // Keep static until dropped 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); // Add to grid immediately lastDroppedFruit = activeFruit; lastDroppedHasMerged = false; chargeCounter++; updateChargedBallDisplay(); if (chargeCounter >= 9 && !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 >= 9) { 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); // Add charged fruit to grid chargeCounter = 0; resetChargedBalls(); readyToReleaseCharged = false; } activeFruit = null; // Clear active fruit *before* creating next createNextFruit(); } function applyDropPhysics(fruit, forceMultiplier) { var angle = (Math.random() * 20 - 10) * (Math.PI / 180); fruit.vx = Math.sin(angle) * forceMultiplier; fruit.vy = Math.abs(Math.cos(angle) * forceMultiplier); fruit.safetyPeriod = false; // Start in safety period 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: 80, fill: 0x000000 }); scoreText.anchor.set(0.5, 0); LK.gui.top.addChild(scoreText); scoreText.y = 30; 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() { // Reset surrounded status for all fruits before checking for (var k = 0; k < fruits.length; k++) { if (fruits[k]) { fruits[k].neighboringFruits = 0; fruits[k].neighborContacts = fruits[k].neighborContacts || []; fruits[k].neighborContacts = []; // Reset neighbor contacts list } } 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 combinedHalfWidths = fruit1HalfWidth + fruit2HalfWidth; var combinedHalfHeights = fruit1HalfHeight + fruit2HalfHeight; 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; // Use larger exponent for mass calculation to increase size effect var mass1 = Math.pow(fruit1.type.size, 1.8); var mass2 = Math.pow(fruit2.type.size, 1.8); var totalMass = mass1 + mass2; var impulseRatio1 = totalMass > 0 ? mass2 / totalMass : 0.5; var impulseRatio2 = totalMass > 0 ? mass1 / totalMass : 0.5; var impulse1 = impulse * impulseRatio1; var impulse2 = impulse * impulseRatio2; var sizeDifference = Math.abs(fruit1.type.size - fruit2.type.size) / Math.max(fruit1.type.size, fruit2.type.size, 1); // Enhance size impact on collision if (fruit1.type.size < fruit2.type.size) { impulse1 *= 1 + sizeDifference * 0.7; } else if (fruit2.type.size < fruit1.type.size) { impulse2 *= 1 + sizeDifference * 0.7; } 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); // Track that these fruits are neighbors for stabilization detection fruit1.neighboringFruits = (fruit1.neighboringFruits || 0) + 1; fruit2.neighboringFruits = (fruit2.neighboringFruits || 0) + 1; // Track detailed neighbor information for improved stability detection 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); } // Additional stability check for network of fruits if (fruit1.neighborContacts.length >= 3 || fruit2.neighborContacts.length >= 3) { // Apply additional stabilizing forces to complex fruit networks 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 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; } if (fruit.safetyPeriod !== true) { if (fruit.vy > 0.1) { fruit.safetyPeriod = false; continue; } else { fruit.safetyPeriod = true; } } if (fruit.safetyPeriod === true) { 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); // Don't add to spatial grid until it's dropped } function pushPineapple() { if (!pineappleActive && pineapple) { var step = mergeCounter; var totalSteps = 10; 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 = []; // Clear fruits array first // Clear existing children from the game except UI elements if needed // (Assuming LK.Game handles cleanup or we manage it here) // Example: game.removeChildren(); // Be careful with this, might remove UI chargeCounter = 0; if (chargedBallUI) { chargedBallUI.destroy(); } chargedBallUI = null; readyToReleaseCharged = false; lastScoreCheckForCoconut = 0; lastDroppedFruit = null; lastDroppedHasMerged = false; mergeCounter = 0; isClickable = true; if (spatialGrid) { spatialGrid.clear(); } else { // Calculate the optimal cell size based on average fruit size 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; // Set cell size to approximately 110% of average fruit size for optimal collision checks var optimalCellSize = Math.ceil(avgFruitSize * 1.1); spatialGrid = new SpatialGrid(optimalCellSize); } LK.playMusic('bgmusic'); // Destroy old boundaries if they exist before creating new ones if (wallLeft) { wallLeft.destroy(); } if (wallRight) { wallRight.destroy(); } if (gameFloor) { gameFloor.destroy(); } if (gameOverLine) { gameOverLine.destroy(); } if (pineapple) { pineapple.destroy(); } if (trajectoryLine) { trajectoryLine.destroy(); } setupBoundaries(); setupUI(); // Setup UI after clearing old elements setupPineapple(); trajectoryLine = game.addChild(new TrajectoryLine()); trajectoryLine.createDots(); updateScoreDisplay(); // Clear active fruit *before* creating the first one activeFruit = null; createNextFruit(); // Reset charged ball display explicitly after setup resetChargedBalls(); } 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; // Start static for animation game.addChild(coconut); fruits.push(coconut); // Don't add to grid yet, wait for animation finish coconut.safetyPeriod = false; // Initial state 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; } // Check if coconut still exists coconut.isStatic = false; coconut.vy = -2; // Small bounce coconut.vx = (Math.random() * 2 - 1) * 1.5; spatialGrid.insertObject(coconut); // Add to grid *after* becoming dynamic LK.setTimeout(function () { if (coconut && fruits.includes(coconut)) { coconut.immuneToGameOver = false; } }, 1000); } }); } var lastScoreCheckForCoconut = 0; // Event handlers game.down = function (x, y) { if (activeFruit && !gameOver) { // Prevent interaction if game over isDragging = true; game.move(x, y); // Initial move to cursor } }; 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)); // Keep Y fixed at the top while dragging activeFruit.y = dropPointY + 200; if (trajectoryLine) { trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y); } } }; game.up = function () { if (isDragging && activeFruit && isClickable && !gameOver) { dropFruit(); } isDragging = false; // Always reset dragging on up }; function updatePhysics() { // Periodically rebuild the spatial grid to prevent memory leaks if (spatialGrid && Date.now() - spatialGrid.lastRebuildTime > spatialGrid.rebuildInterval) { spatialGrid.rebuildGrid(fruits); } // First pass: Update physics and boundary collisions 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); } // Process fruit-to-fruit collisions checkFruitCollisions(); // Second pass: Stabilize fruits based on surroundings for (var i = 0; i < fruits.length; i++) { var fruit = fruits[i]; if (fruit && !fruit.isStatic && !fruit.merging) { // Initialize stabilization tracking variables if needed if (fruit.surroundedFrames === undefined) { fruit.surroundedFrames = 0; } // Combined check for all stabilization scenarios in one place 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; // Determine stabilization scenario var stabilizationScenario = boundaryContact ? "boundary" : floorProximity ? "floor" : hasMultipleNeighbors ? "surrounded" : "free"; // Apply appropriate stabilization based on scenario switch (stabilizationScenario) { case "boundary": // Already handled by boundary collisions fruit.surroundedFrames = 0; break; case "floor": // Already handled by floor collision fruit.surroundedFrames = 0; break; case "surrounded": // Floating fruit surrounded by other fruits fruit.surroundedFrames++; // Stabilization strength increases with duration var stabilizationStrength = Math.min(0.95, 0.7 + fruit.surroundedFrames * 0.02); // Apply stronger stabilization for fruits in a complex network if (fruit.neighborContacts.length >= 3) { stabilizationStrength = Math.min(0.98, stabilizationStrength + 0.05); } // Apply stabilization fruit.vx *= stabilizationStrength; fruit.vy *= stabilizationStrength; fruit.angularVelocity *= stabilizationStrength; // Stop fruit completely if it's been surrounded and nearly stationary for a while if (fruit.surroundedFrames > 8 && isSlowMoving || fruit.surroundedFrames > 15) { fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; fruit.rotation = Math.round(fruit.rotation / (Math.PI / 2)) * (Math.PI / 2); } break; case "free": // Reset surrounded counter for free-moving fruits fruit.surroundedFrames = 0; break; } // Update spatial grid with new position spatialGrid.updateObject(fruit); } } } game.update = function () { if (gameOver) { return; } var currentScore = LK.getScore(); if (currentScore >= lastScoreCheckForCoconut + 500) { lastScoreCheckForCoconut = Math.floor(currentScore / 500) * 500; spawnCoconut(); } else { // Keep track of the current score threshold even when not spawning if (currentScore > lastScoreCheckForCoconut) { lastScoreCheckForCoconut = Math.floor(currentScore / 500) * 500; } } updatePhysics(); checkGameOver(); }; // Initialize the game initGame(); function removeFruitFromGame(fruit) { var index = fruits.indexOf(fruit); if (index !== -1) { fruits.splice(index, 1); } spatialGrid.removeObject(fruit); fruit.destroy(); }
===================================================================
--- original.js
+++ change.js
@@ -136,11 +136,13 @@
left: false,
right: false,
floor: false
};
+ // Reset boundary contacts at start of each check
fruit._boundaryContacts.left = false;
fruit._boundaryContacts.right = false;
fruit._boundaryContacts.floor = false;
+ // Check each boundary collision
self.checkLeftWallCollision(fruit, walls.left, effectiveWidth);
self.checkRightWallCollision(fruit, walls.right, effectiveWidth);
self.checkFloorCollision(fruit, floor, effectiveHeight);
// Update wall contact frames counter based on contact with any boundary
@@ -150,11 +152,15 @@
// Apply progressive friction based on contact duration
var progressiveFriction = Math.min(0.85, 0.65 + fruit.wallContactFrames * 0.01);
fruit.angularVelocity *= progressiveFriction;
// Apply more aggressive stabilization for prolonged contact
+ // But still let gravity work if not on floor
if (fruit.wallContactFrames > 10) {
fruit.vx *= 0.9;
- fruit.vy *= 0.9;
+ // Only dampen vertical velocity if on floor
+ if (fruit._boundaryContacts.floor) {
+ fruit.vy *= 0.9;
+ }
}
} else {
// Gradually decrease contact counter when not touching walls
fruit.wallContactFrames = Math.max(0, fruit.wallContactFrames - 1);
@@ -201,19 +207,23 @@
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;
+ // Bounce based on elasticity and size (larger fruits bounce less)
+ var fruitLevel = getFruitLevel(fruit);
+ var bounceDamping = 0.5 * (1 - (fruitLevel - 1) * 0.05);
+ fruit.vy = -fruit.vy * fruit.elasticity * bounceDamping;
// Track that fruit is touching floor
fruit._boundaryContacts.floor = true;
// Update angular velocity based on horizontal movement
if (Math.abs(fruit.vx) > 0.5) {
fruit.angularVelocity = fruit.vx * 0.01;
}
fruit.angularVelocity *= fruit.groundAngularFriction * 0.7;
- // Progressive stabilization based on contact duration and velocity
+ // Only stop vertical movement if the bounce is small enough
+ var bounceMagnitude = Math.abs(fruit.vy);
var restThreshold = fruit.wallContactFrames > 5 ? 1.5 : 2.5;
- if (Math.abs(fruit.vy) < restThreshold) {
+ if (bounceMagnitude < restThreshold) {
fruit.vy = 0;
fruit.vx *= 0.6;
// More aggressive stabilization for fruits that have been on floor for a while
if (fruit.wallContactFrames > 8) {
@@ -567,24 +577,30 @@
fruit._stabilizeMetrics.surroundedDuration = Math.max(0, fruit._stabilizeMetrics.surroundedDuration - 1);
}
// Determine stabilization level based on combined factors
var totalStabilizationScore = fruit._stabilizeMetrics.consecutiveSmallMovements * 1.0 + fruit._stabilizeMetrics.wallContactDuration * 0.8 + fruit._stabilizeMetrics.surroundedDuration * 1.2 + fruit._stabilizeMetrics.restingDuration * 0.5;
- if (totalStabilizationScore > 15) {
+ // Make stabilization thresholds higher to prevent premature stabilization
+ if (totalStabilizationScore > 20) {
stabilizationLevel = 2; // Full stabilization
- } else if (totalStabilizationScore > 8) {
+ } else if (totalStabilizationScore > 12) {
stabilizationLevel = 1; // Partial stabilization
}
// Apply stabilization based on level
if (stabilizationLevel > 0) {
// Dampen movement increasingly with stabilization level
var dampFactor = stabilizationLevel === 2 ? 0.5 : 0.8;
fruit.vx *= dampFactor;
- fruit.vy *= dampFactor;
+ // Don't dampen vertical velocity as aggressively to allow gravity to work
+ fruit.vy *= stabilizationLevel === 2 ? 0.7 : 0.9;
fruit.angularVelocity *= dampFactor;
- // For full stabilization, completely stop if movement is minimal
+ // For full stabilization, only stop horizontal movement completely
+ // Let gravity continue to work vertically unless firmly on ground
if (stabilizationLevel === 2 && movementMagnitude < 0.1) {
fruit.vx = 0;
- fruit.vy = 0;
+ // Only zero out vertical velocity if fruit is touching floor
+ if (fruit._boundaryContacts && fruit._boundaryContacts.floor) {
+ fruit.vy = 0;
+ }
fruit.angularVelocity = 0;
fruit.rotation = Math.round(fruit.rotation / (Math.PI / 2)) * (Math.PI / 2);
return true;
}
@@ -599,11 +615,10 @@
fruit.lastVy = fruit.vy;
// Scale gravity based on fruit level/size
var fruitLevel = getFruitLevel(fruit);
var gravityMultiplier = 0.8 + fruitLevel * 0.2;
- // Apply gravity with scaling multiplier
+ // Always apply gravity regardless of stabilization state
fruit.vy += fruit.gravity * gravityMultiplier;
- // Update position with current velocity
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);
@@ -618,14 +633,16 @@
} else {
fruit.angularVelocity *= 0.8;
}
fruit.rotation += fruit.angularVelocity;
+ // Apply friction but don't completely kill momentum too easily
fruit.vx *= fruit.friction;
fruit.vy *= fruit.friction;
- // Velocity thresholds for stopping
+ // Velocity thresholds for stopping - higher threshold for vertical to let gravity work
if (Math.abs(fruit.vx) < 0.1) {
fruit.vx = 0;
}
+ // Only stop vertical movement if very slow AND near the floor
if (Math.abs(fruit.vy) < 0.1 && fruit.y > gameHeight - 300) {
fruit.vy = 0;
}
self.handleRotationDamping(fruit);