Code edit (2 edits merged)
Please save this source code
User prompt
move the active fruit lower by 300 pixels as it's currently starting intersecting the top edge of the screen
User prompt
Consider reducing the number of spatialGrid.getPotentialCollisions calls within the trajectory update loop. Perhaps only check for collisions with other fruits every Nth dot, or use a simpler, broader phase check first. Explore if you can project the path mathematically further before needing to do fine-grained collision checks for the trajectory. Slightly reducing self.maxDots in TrajectoryLine if it's very high can also give a quick win if trajectory calculation is a bottleneck.
User prompt
When two fruits are in contact (colliding), there should be a frictional force that acts to reduce their relative angular velocities, just like ground friction does for fruits touching the floor. - This friction should be applied every frame while the fruits are in contact, gradually reducing their angular velocities toward each other (and toward zero if both are spinning). #### 2. **Implementation Steps** - **During Collision Resolution:** In your main fruit-to-fruit collision loop (where you already resolve overlaps and impulses), add a step after the main collision response to apply rotational friction. - **How to Apply:** For each pair of colliding fruits: - Calculate the difference in their angular velocities. - Apply a small fraction of this difference in the opposite direction to each fruit, simulating friction. - Optionally, also apply a small general angular damping to both fruits when in contact, even if their angular velocities are similar. - **Example Pseudocode:** // After resolving collision impulses: var angularDifference = fruit1.angularVelocity - fruit2.angularVelocity; var frictionStrength = 0.02; // Tune this value for desired effect // Apply friction to both fruits fruit1.angularVelocity -= angularDifference * frictionStrength; fruit2.angularVelocity += angularDifference * frictionStrength; // Optionally, apply a little extra damping fruit1.angularVelocity *= 0.98; fruit2.angularVelocity *= 0.98; ``` - It ensures that fruits stacked in mid-air will not spin forever, but will come to rest naturally. #### 4. **Where to Place This Logic** - Place this friction logic inside your fruit-to-fruit collision handling code, after the main collision response (impulse/separation). - Make sure it only applies when fruits are actually overlapping/colliding. #### 5. **Tuning** - The friction strength (the multiplier) may need to be adjusted for the best feel. Too high, and fruits will stop spinning instantly; too low, and they’ll spin for too long.
Code edit (1 edits merged)
Please save this source code
User prompt
make the fruits fall faster
Code edit (1 edits merged)
Please save this source code
User prompt
The way fruits collide with each other in checkFruitCollisions needs a review. The current rectangular-based collision detection can lead to perceived gaps or unnatural interactions, especially with rotation. If the platform or engine tools allow, or if you can implement a simpler version, a more circular collision model would be beneficial. implement these please
User prompt
Ava, let's take a step back from the intricate adjustments in the current physics. The goal is to establish a more robust and predictable foundation. Focus on a simpler set of rules for gravity, basic friction, and collision response first. Try to get 80% of the desired feel with a much smaller, more generalized set of physics principles. Review the way forces like gravity are scaled by fruit level; ensure these scalings are smooth and don't cause overly dramatic changes between fruit tiers.
User prompt
Strict Sleep Condition for Rotation: Action: When checking if a fruit should go to isSleeping (in PhysicsComponent.stabilizeFruit), ensure that Math.abs(fruit.angularVelocity) is extremely close to zero (e.g., less than GAME_CONSTANTS.ANGULAR_STOP_THRESHOLD * 0.5) for the required number of SLEEP_DELAY_FRAMES. Reasoning: A fruit shouldn't "sleep" if it's still visibly, even if slowly, rotating.
User prompt
Collision-Induced Spin (checkFruitCollisions): Action: Review the GAME_CONSTANTS.ROTATION_TRANSFER_FACTOR. If it's too high, even minor glancing blows can cause too much spin. Try reducing it slightly. Action: Ensure that the angularImpulse is primarily driven by a significant tangentialComponent of the collision. Small, direct hits shouldn't cause much spin. The existing impactFactor logic attempts this; ensure its thresholds are effective.
User prompt
Rolling on Floor (PhysicsComponent.handleRotationDamping): Action: Ensure the rollFactor is not too high, preventing excessive rotation from small vx values. It should feel like the fruit is rolling, not spinning wildly from a tiny push. The current scaling with fruitLevel (larger fruits roll less) is good. Action: Make sure the blending towards idealRollingVelocity is not too aggressive if angularVelocity is already low, to prevent it from "kicking up" rotation unnecessarily.
User prompt
Enhance Rotational Friction in Collisions (checkFruitCollisions): Increase Tangential Friction Impulse: Action: Increase the GAME_CONSTANTS.INTER_FRUIT_FRICTION factor used for calculating frictionImpulse from tangentVelocity. Try values like 0.15 or 0.2. Reasoning: This directly simulates more "grip" or "roughness" between colliding fruits, reducing relative spin. Apply Angular Damping During Collision Resolution: Action: After applying the linear and angular impulses from a collision, additionally multiply both fruit1.angularVelocity and fruit2.angularVelocity by a damping factor (e.g., 0.90 or 0.85). Hint: // ... after angular impulses are applied ... fruit1.angularVelocity *= 0.85; // Example damping fruit2.angularVelocity *= 0.85; Use code with caution. JavaScript Reasoning: Helps to settle rotations that are induced by the collision itself more quickly. Consider "Static Friction" for Rotation (Advanced): Concept: If two fruits are in contact and their relative angular velocity is very low (or one is trying to start rotating against a stationary one), apply a stronger initial resistance to rotation (like static friction overcoming kinetic). Action (Simplified approach): If Math.abs(fruit1.angularVelocity - fruit2.angularVelocity) is below a tiny threshold AND they are in contact, apply a stronger damping to both (e.g., *= 0.5). Reasoning: Prevents fruits from easily initiating spin against each other if they are already settled together.
User prompt
Extreme Damping at Near-Zero Linear Speed: Action: If movementMagnitude is extremely low (e.g., < 0.1), apply a very aggressive angular damping factor (e.g., fruit.angularVelocity *= 0.1 or even 0.05). Reasoning: If the fruit isn't moving linearly, any residual spin should die out almost immediately. Stricter Angular Stop: Action: Lower the GAME_CONSTANTS.ANGULAR_STOP_THRESHOLD even further (e.g., to 0.001). Action: Reduce the restFramesThreshold in handleRotationDamping for setting angularVelocity to 0 to just 1 or 2 frames. Reasoning: Forces small angular velocities to zero much faster.
User prompt
Strengthen Angular Damping When "Trapped" or Nearly Still (PhysicsComponent.handleRotationDamping): Neighborhood-Aware Damping: Action: Inside handleRotationDamping, significantly increase the angular damping factor if a fruit has multiple neighborContacts AND its movementMagnitude is very low. The more neighbors a trapped, slow fruit has, the faster its rotation should stop. Hint: if (fruit.neighborContacts && fruit.neighborContacts.length >= 2 && movementMagnitude < 0.5 /* low movement threshold */) { var neighborDampFactor = 0.1 + Math.max(0, 0.4 - fruit.neighborContacts.length * 0.05); // More neighbors = stronger damp fruit.angularVelocity *= neighborDampFactor; } Use code with caution. Reasoning: Simulates the idea that a fruit wedged between others cannot easily spin.
User prompt
Uniform Logic: Scrutinize all physics-related code sections (especially PhysicsComponent and CollisionComponent) to ensure there are no remaining if (fruitLevel === X) type conditions that create special rules. All scaling should be continuous based on fruitLevel. The exception is the initial fruit drop probabilities and specific merge sounds/effects in FruitBehavior.
User prompt
Remove hasBounced: Confirm this flag and its associated logic have been entirely removed from Fruit class, CollisionComponent, and PhysicsComponent.
User prompt
Exaggerated Mass in Collisions (checkFruitCollisions): Action: Ensure the mass1 and mass2 calculations use a strongly exponential factor of level. Hint: mass = Math.pow(level, 3.0) or even Math.pow(level, 3.5). Action: When calculating impulseRatio1 and impulseRatio2 from these masses, the effect of mass difference will become very pronounced. A heavy fruit will impart a large impulse to a light fruit while receiving very little itself. Action: Remove any extra, explicit force multipliers based on levelDifference or sizeDifference for bounce effects. The exaggerated mass difference in the impulse calculation should now be the primary driver of how fruits of different "weights" interact. Reasoning: Simplifies collision response and makes "weight" an emergent property of the mass-based impulse exchange. Level-Scaled Elasticity (Verify in Fruit constructor): Action: Review the elasticity calculation. Ensure that higher-level (heavier) fruits have lower elasticity, making them less bouncy and contributing to their "heavy" feel. Hint: self.elasticity = BASE_ELASTICITY - fruitLevel * ELASTICITY_LEVEL_FACTOR; (Ensure it's clamped to a minimum positive value).
User prompt
Implement Pronounced Level-Based Weight: Exaggerated Mass for Gravity & Inertia (PhysicsComponent.apply): Action: Use a higher power for fruitLevel when calculating gravityMultiplier. Hint: gravityMultiplier = 1 + Math.pow(fruitLevel, 1.8) * NEW_GRAVITY_FACTOR; (Tune NEW_GRAVITY_FACTOR). Action: Use a higher power for fruitLevel when calculating inertiaResistance. Hint: inertiaResistance = Math.min(0.98, Math.pow(fruitLevel, 2.0) * NEW_INERTIA_FACTOR); (Tune NEW_INERTIA_FACTOR). Reasoning: This will make higher-level fruits feel substantially heavier, fall faster, and be much harder to push.
User prompt
Rotation from Rolling Only: Action: In PhysicsComponent.apply (or handleRotationDamping), the calculation targetAngularVelocity = fruit.vx * rotationFactor should only be performed and applied if fruit._boundaryContacts.floor is true (and Math.abs(fruit.vx) is above a small threshold to prevent rotation when barely moving on the floor). Action: If the fruit is not rolling on the floor (e.g., mid-air, on a wall, or sliding on the floor without enough vx), do not apply this targetAngularVelocity. Instead, its angularVelocity should only be affected by collision impulses and strong damping. Reasoning: Fruits in the air or sliding shouldn't magically generate rolling rotation. Aggressive Angular Damping When Not Rolling: Action: In PhysicsComponent.handleRotationDamping, if the fruit is not considered to be rolling (based on the condition above), apply a much stronger angular damping factor (e.g., fruit.angularVelocity *= 0.6 or 0.5). Reasoning: Spin in the air or from non-rolling slides should decay very rapidly. Strict Angular Stop Condition: Action: In PhysicsComponent.handleRotationDamping, significantly lower the angularThreshold for setting angularVelocity to 0 (e.g., to 0.001) and reduce restFramesThreshold to 1 or 2. Reasoning: Stops tiny, imperceptible rotations quickly.
User prompt
Drastic Low-Velocity Damping (Before Stabilization): Action: Inside PhysicsComponent.apply, before the call to stabilizeFruit, add a very aggressive damping section for fruits with extremely low velocities that are not mid-air. Hint: if (!isMidAir && movementMagnitude < 0.2 /* low threshold */) { fruit.vx *= 0.5; // Very strong damping fruit.vy *= 0.5; fruit.angularVelocity *= 0.4; if (movementMagnitude < 0.05) { // Extremely low fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; } } Use code with caution. Reasoning: This actively fights the micro-movements that prevent the stabilization criteria from being met.
User prompt
Implement "Sleep State" More Aggressively: Action: In PhysicsComponent.stabilizeFruit: When a fruit meets the criteria for full stabilization (stabilizationLevel === 2 and movement below stopThreshold) AND is not isMidAir: Immediately set fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0;. Set fruit.isFullyStabilized = true;. Increment fruit._sleepCounter. If fruit._sleepCounter >= GAME_CONSTANTS.SLEEP_DELAY_FRAMES, set fruit.isSleeping = true;. If a fruit does not meet full stabilization criteria in a frame, reset fruit._sleepCounter = 0; and ensure fruit.isSleeping = false;. Action: At the very start of PhysicsComponent.apply: if (fruit.isSleeping) { fruit.vx=0; fruit.vy=0; fruit.angularVelocity=0; return; } Action: Ensure wake-up conditions (in checkFruitCollisions and removeFruitFromGame) are robust. A significant collision impulse (above GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD) must set isSleeping = false, isFullyStabilized = false, and reset _sleepCounter. Reasoning: This creates a very definitive "stop" and prevents physics calculations on fruits that should be at rest.
User prompt
Strengthen Overall Friction: Action: Significantly decrease the values for GAME_CONSTANTS.FRICTION and GAME_CONSTANTS.ANGULAR_FRICTION. Make them noticeably lower (e.g., FRICTION from 0.9 to 0.85 or even 0.8, ANGULAR_FRICTION to 0.75 or 0.7). Reasoning: This will bleed off all motion much faster.
User prompt
small level 1 fruits, tend to be smooshed inside other fruits, being pressed so hard, they are forced to overlap other fruits. fruits should never overlap
User prompt
Remove hasBounced: Delete the hasBounced property from the Fruit class and remove all logic referencing it in CollisionComponent and PhysicsComponent. Rely on elasticity and damping. Consolidate Constants: Ensure all magic numbers related to physics are defined in GAME_CONSTANTS. Review Order: Double-check the order of operations within updatePhysics and PhysicsComponent.apply to ensure forces, collisions, damping, stabilization, and position updates happen logically. Applying friction/damping after stabilization might help solidify the halting state.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var ChargedBallUI = Container.expand(function () { var self = Container.call(this); self.chargeNeededForRelease = GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE; self.currentCharge = 0; self.isReadyToRelease = false; self.pulseAnimationActive = false; self.initialize = function () { self.portalAsset = self.attachAsset('portal', { anchorX: 0.5, anchorY: 0.5 }); self.portalAsset.x = GAME_CONSTANTS.GAME_WIDTH / 2 + GAME_CONSTANTS.PORTAL_UI_X_OFFSET; self.portalAsset.alpha = 0; self.portalAsset.scaleX = 0; self.portalAsset.scaleY = 0; self.y = GAME_CONSTANTS.PORTAL_UI_Y; }; self.updateChargeDisplay = function (chargeCount) { self.currentCharge = chargeCount; var remainingCount = Math.max(0, self.chargeNeededForRelease - self.currentCharge); var progressPercent = (self.chargeNeededForRelease - remainingCount) / self.chargeNeededForRelease; var targetScale = progressPercent; if (progressPercent > 0 && self.portalAsset.alpha === 0) { self.portalAsset.alpha = 1; } tween(self.portalAsset, { scaleX: targetScale, scaleY: targetScale, alpha: progressPercent }, { duration: GAME_CONSTANTS.PORTAL_TWEEN_DURATION, easing: tween.easeOut }); if (remainingCount === 0 && !self.isReadyToRelease) { self.setReadyState(true); } }; self.setReadyState = function (isReady) { self.isReadyToRelease = isReady; if (isReady) { tween(self.portalAsset, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0, rotation: Math.PI * 2 }, { duration: GAME_CONSTANTS.PORTAL_TWEEN_DURATION, easing: tween.easeOut }); self.startPulseAnimation(); } }; self.startPulseAnimation = function () { if (self.pulseAnimationActive) return; self.pulseAnimationActive = true; self._pulseText(); }; self._pulseText = function () { if (!self.isReadyToRelease) { self.pulseAnimationActive = false; return; } tween(self.portalAsset, { scaleX: 1.3, scaleY: 1.3 }, { duration: GAME_CONSTANTS.PORTAL_PULSE_DURATION, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.isReadyToRelease) { self.pulseAnimationActive = false; return; } tween(self.portalAsset, { scaleX: 1.0, scaleY: 1.0 }, { duration: GAME_CONSTANTS.PORTAL_PULSE_DURATION, easing: tween.easeInOut, onFinish: self._pulseText }); } }); }; self.reset = function () { self.isReadyToRelease = false; self.currentCharge = 0; self.pulseAnimationActive = false; tween(self.portalAsset, { alpha: 0 }, { duration: 200, easing: tween.easeOut }); tween(self.portalAsset, { scaleX: 0, scaleY: 0 }, { duration: 200, easing: tween.easeOut }); }; self.initialize(); return self; }); var CollisionComponent = Container.expand(function () { var self = Container.call(this); 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 = Math.max(0, fruitLevel - 4) * 2; 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; 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.75, 0.55 + fruit.wallContactFrames * 0.015); fruit.angularVelocity *= progressiveFriction; if (fruit.wallContactFrames > 15) { fruit.vx *= 0.75; fruit.vy *= 0.75; fruit.angularVelocity *= 0.5; } else if (fruit.wallContactFrames > 10) { fruit.vx *= 0.8; fruit.vy *= 0.85; fruit.angularVelocity *= 0.7; } else if (fruit.wallContactFrames > 5) { 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) { var incomingVx = fruit.vx; fruit.x = leftBoundary; fruit.vx = -incomingVx * fruit.elasticity * 0.7; if (Math.abs(incomingVx) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) { LK.getSound('bounce').play(); } var angularImpactMultiplier = 0.0025 * (1 + (0.9 - fruit.elasticity) * 5); fruit.angularVelocity += fruit.vy * angularImpactMultiplier * 0.25; fruit.angularVelocity *= GAME_CONSTANTS.GROUND_ANGULAR_FRICTION * 0.4; fruit._boundaryContacts.left = true; } 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) { var incomingVx = fruit.vx; fruit.x = rightBoundary; fruit.vx = -incomingVx * fruit.elasticity * 0.7; if (Math.abs(incomingVx) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) { LK.getSound('bounce').play(); } var angularImpactMultiplier = 0.0025 * (1 + (0.9 - fruit.elasticity) * 5); fruit.angularVelocity -= fruit.vy * angularImpactMultiplier * 0.25; fruit.angularVelocity *= GAME_CONSTANTS.GROUND_ANGULAR_FRICTION * 0.4; fruit._boundaryContacts.right = true; } }; self.checkFloorCollision = function (fruit, floor, effectiveHeight) { var floorCollisionY = floor.y - floor.height / 2 - effectiveHeight; if (fruit.y > floorCollisionY) { var incomingVy = fruit.vy; fruit.y = floorCollisionY; fruit.vy = -incomingVy * fruit.elasticity * 0.5; if (Math.abs(incomingVy) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) { LK.getSound('bounce').play(); } fruit._boundaryContacts.floor = true; if (Math.abs(fruit.vx) > 0.5) { fruit.angularVelocity = fruit.vx * 0.008; } fruit.angularVelocity *= GAME_CONSTANTS.GROUND_ANGULAR_FRICTION * 0.4; var restThreshold = fruit.wallContactFrames > 5 ? 1.5 : 2.5; if (Math.abs(fruit.vy) < restThreshold) { fruit.vy = 0; fruit.vx *= 0.6; if (fruit.wallContactFrames > 8) { fruit.vx *= 0.75; } } var angularRestThreshold = fruit.wallContactFrames > 5 ? 0.012 : 0.018; 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 = GAME_CONSTANTS.EVOLUTION_LINE_Y; self.x = GAME_CONSTANTS.GAME_WIDTH / 2; // Remove the X_OFFSET to center properly var fruitTypes = [FruitTypes.CHERRY, FruitTypes.GRAPE, FruitTypes.APPLE, FruitTypes.ORANGE, FruitTypes.WATERMELON, FruitTypes.PINEAPPLE, FruitTypes.MELON, FruitTypes.PEACH, FruitTypes.COCONUT, FruitTypes.DURIAN]; var totalWidth = 0; var fruitIcons = []; for (var i = 0; i < fruitTypes.length; i++) { var fruitType = fruitTypes[i]; var fruitIcon = LK.getAsset(fruitType.id, { anchorX: 0.5, anchorY: 0.5 }); var scale = Math.min(GAME_CONSTANTS.EVOLUTION_ICON_MAX_SIZE / fruitIcon.width, GAME_CONSTANTS.EVOLUTION_ICON_MAX_SIZE / fruitIcon.height); fruitIcon.scaleX = scale; fruitIcon.scaleY = scale; totalWidth += fruitIcon.width * scale; if (i < fruitTypes.length - 1) { totalWidth += GAME_CONSTANTS.EVOLUTION_ICON_SPACING; } fruitIcons.push(fruitIcon); } var currentX = -totalWidth / 2; for (var i = 0; i < fruitIcons.length; i++) { var icon = fruitIcons[i]; icon.x = currentX + icon.width * icon.scaleX / 2; icon.y = 0; self.addChild(icon); currentX += icon.width * icon.scaleX + GAME_CONSTANTS.EVOLUTION_ICON_SPACING; } }; return self; }); var FireElement = Container.expand(function (initX, initY, zIndex) { var self = Container.call(this); self.baseX = initX || 0; self.baseY = initY || 0; self.zIndex = zIndex || 0; self.movementRange = 30 + Math.random() * 20; self.movementSpeed = 0.3 + Math.random() * 0.4; self.direction = Math.random() > 0.5 ? 1 : -1; self.alphaMin = GAME_CONSTANTS.FIRE_ALPHA_MIN; self.alphaMax = GAME_CONSTANTS.FIRE_ALPHA_MAX; self.flickerSpeed = GAME_CONSTANTS.FIRE_FLICKER_SPEED_BASE + Math.random() * GAME_CONSTANTS.FIRE_FLICKER_SPEED_RANDOM; self.frameIndex = 0; self.frameTimer = null; self.frameDuration = GAME_CONSTANTS.FIRE_FRAME_DURATION; self.initialize = function () { self.fireAsset = self.attachAsset('fire', { anchorX: 0.5, anchorY: 1.0 }); self.fireAsset2 = self.attachAsset('fire_2', { anchorX: 0.5, anchorY: 1.0 }); self.fireAsset2.visible = false; self.x = self.baseX; self.y = self.baseY; self.startAlphaFlicker(); self.startFrameAnimation(); }; self.update = function () { self.x += self.movementSpeed * self.direction; if (Math.abs(self.x - self.baseX) > self.movementRange) { self.direction *= -1; } }; self.startFrameAnimation = function () { if (self.frameTimer) LK.clearInterval(self.frameTimer); self.frameTimer = LK.setInterval(function () { self.toggleFrame(); }, self.frameDuration); }; self.toggleFrame = function () { self.frameIndex = (self.frameIndex + 1) % 2; self.fireAsset.visible = self.frameIndex === 0; self.fireAsset2.visible = self.frameIndex === 1; }; self.startAlphaFlicker = function () { if (self.flickerTween) self.flickerTween.stop(); var startDelay = Math.random() * 500; LK.setTimeout(function () { self.flickerToMax(); }, startDelay); }; self.flickerToMax = function () { self.flickerTween = tween(self, { alpha: self.alphaMax }, { duration: self.flickerSpeed, easing: tween.easeInOut, onFinish: self.flickerToMin }); }; self.flickerToMin = function () { self.flickerTween = tween(self, { alpha: self.alphaMin }, { duration: self.flickerSpeed, easing: tween.easeInOut, onFinish: self.flickerToMax }); }; self.destroy = function () { if (self.flickerTween) self.flickerTween.stop(); if (self.frameTimer) { LK.clearInterval(self.frameTimer); self.frameTimer = null; } Container.prototype.destroy.call(this); }; 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 = GAME_CONSTANTS.ANGULAR_FRICTION; self.groundAngularFriction = GAME_CONSTANTS.GROUND_ANGULAR_FRICTION; var currentLevel = getFruitLevel(self); self.gravity = GAME_CONSTANTS.BASE_GRAVITY * (1 + Math.pow(currentLevel, 1.8) * 0.35); self.friction = GAME_CONSTANTS.FRICTION; self.rotationRestCounter = physics.rotationRestCounter; self.maxAngularVelocity = GAME_CONSTANTS.MAX_ANGULAR_VELOCITY; self.isStatic = physics.isStatic; // Simplified continuous elasticity calculation based on fruit level self.elasticity = GAME_CONSTANTS.ELASTICITY_HIGH - currentLevel * GAME_CONSTANTS.ELASTICITY_DECREASE_FACTOR * 1.2; self.elasticity = Math.max(0.08, self.elasticity); // Lower minimum elasticity for heavier fruits self.wallContactFrames = collision.wallContactFrames; self.merging = mergeHandler.merging; self.mergeGracePeriodActive = mergeHandler.mergeGracePeriodActive; self.fromChargedRelease = mergeHandler.fromChargedRelease; self.safetyPeriod = false; self.immuneToGameOver = false; self.isFullyStabilized = false; self.isSleeping = false; self._sleepCounter = 0; 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(f1, f2, x, y) { return self.standardMerge(f1, f2, x, y); } }, GRAPE: { onMerge: function onMerge(f1, f2, x, y) { return self.standardMerge(f1, f2, x, y); } }, APPLE: { onMerge: function onMerge(f1, f2, x, y) { return self.standardMerge(f1, f2, x, y); } }, ORANGE: { onMerge: function onMerge(f1, f2, x, y) { return self.standardMerge(f1, f2, x, y); } }, WATERMELON: { onMerge: function onMerge(f1, f2, x, y) { return self.standardMerge(f1, f2, x, y); } }, PINEAPPLE: { onMerge: function onMerge(f1, f2, x, y) { return self.standardMerge(f1, f2, x, y); }, onSpawn: function onSpawn() {} }, MELON: { onMerge: function onMerge(f1, f2, x, y) { LK.getSound('Smartz').play(); return self.standardMerge(f1, f2, x, y); } }, PEACH: { onMerge: function onMerge(f1, f2, x, y) { LK.getSound('stonks').play(); return self.standardMerge(f1, f2, x, y); } }, COCONUT: { onMerge: function onMerge(f1, f2, x, y) { LK.getSound('ThisIsFine').play(); return self.standardMerge(f1, f2, x, y); }, onSpawn: function onSpawn() { LK.getSound('stonks').play(); } }, DURIAN: { onMerge: function onMerge(f1, f2, x, y) { LK.setScore(LK.getScore() + f1.type.points); updateScoreDisplay(); removeFruitFromGame(f1); removeFruitFromGame(f2); 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 || fruit2.merging) return; // Added check for fruit2 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); if (behaviorHandler && behaviorHandler.onMerge) { // Added check behaviorHandler.onMerge(fruit1, fruit2, midX, midY); } // Ensure cleanup happens correctly, check if fruits still exist before removal if (fruit1 && fruit1.parent && fruit1.type.id.toUpperCase() !== 'DURIAN') removeFruitFromGame(fruit1); if (fruit2 && fruit2.parent) 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 = GAME_CONSTANTS.BASE_GRAVITY; self.friction = GAME_CONSTANTS.FRICTION; self.isStatic = false; self.rotation = 0; self.angularVelocity = 0; self.maxAngularVelocity = GAME_CONSTANTS.MAX_ANGULAR_VELOCITY; self.rotationRestCounter = 0; self.lastVx = 0; self.lastVy = 0; self.isFullyStabilized = false; // Moved here self.stabilizeFruit = function (fruit, movementMagnitude, velocityChange) { var isMidAir = !fruit._boundaryContacts || !fruit._boundaryContacts.floor && !fruit._boundaryContacts.left && !fruit._boundaryContacts.right; if (isMidAir) { fruit.isFullyStabilized = false; // Ensure mid-air fruits are never marked stabilized fruit._sleepCounter = 0; // Reset sleep counter when in mid-air fruit.isSleeping = false; // Ensure fruit is awake when in mid-air return false; } var stabilizationLevel = 0; var fruitLevel = getFruitLevel(fruit); if (!fruit._stabilizeMetrics) { fruit._stabilizeMetrics = { consecutiveSmallMovements: 0, wallContactDuration: 0, surroundedDuration: 0, restingDuration: 0 }; } // Dynamic thresholds with continuous scaling by level var levelScale = Math.pow(fruitLevel, 1.2) / Math.pow(10, 1.2); // Normalized scaling factor var movementThreshold = GAME_CONSTANTS.STABILIZATION_MOVEMENT_THRESHOLD_BASE - levelScale * GAME_CONSTANTS.STABILIZATION_MOVEMENT_THRESHOLD_BASE * 0.7; var angularThreshold = GAME_CONSTANTS.STABILIZATION_ANGULAR_THRESHOLD_BASE - levelScale * GAME_CONSTANTS.STABILIZATION_ANGULAR_THRESHOLD_BASE * 0.7; if (movementMagnitude < movementThreshold && Math.abs(fruit.angularVelocity) < angularThreshold) { var stabilizationRate = GAME_CONSTANTS.STABILIZATION_SCORE_RATE_BASE + fruitLevel * GAME_CONSTANTS.STABILIZATION_SCORE_LEVEL_FACTOR * 1.4; fruit._stabilizeMetrics.consecutiveSmallMovements += stabilizationRate; fruit._stabilizeMetrics.restingDuration += stabilizationRate; } else { fruit._stabilizeMetrics.consecutiveSmallMovements = 0; fruit._stabilizeMetrics.restingDuration = Math.max(0, fruit._stabilizeMetrics.restingDuration - 2); } // Continuous scaling for wall contact and surrounded durations var wallContactRate = GAME_CONSTANTS.STABILIZATION_WALL_SCORE_RATE * 1.4 + Math.pow(fruitLevel, 1.2) * 0.12; var surroundedRate = GAME_CONSTANTS.STABILIZATION_SURROUNDED_SCORE_RATE * 1.4 + Math.pow(fruitLevel, 1.2) * 0.18; if (fruit.wallContactFrames > 0) fruit._stabilizeMetrics.wallContactDuration += wallContactRate;else fruit._stabilizeMetrics.wallContactDuration = Math.max(0, fruit._stabilizeMetrics.wallContactDuration - 1); if (fruit.surroundedFrames > 0) fruit._stabilizeMetrics.surroundedDuration += surroundedRate;else fruit._stabilizeMetrics.surroundedDuration = Math.max(0, fruit._stabilizeMetrics.surroundedDuration - 1); var levelFactor = fruitLevel * GAME_CONSTANTS.STABILIZATION_SCORE_LEVEL_INFLUENCE; 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); // Dynamic thresholds for stabilization based only on fruitLevel - lower for higher-level fruits var fullStabilizationThreshold = GAME_CONSTANTS.STABILIZATION_THRESHOLD_FULL_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_THRESHOLD_FULL_LEVEL_FACTOR * 1.25; var partialStabilizationThreshold = GAME_CONSTANTS.STABILIZATION_THRESHOLD_PARTIAL_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_THRESHOLD_PARTIAL_LEVEL_FACTOR * 1.25; if (totalStabilizationScore > fullStabilizationThreshold) stabilizationLevel = 2;else if (totalStabilizationScore > partialStabilizationThreshold) stabilizationLevel = 1; if (stabilizationLevel > 0) { // Make damping stronger for higher-level fruits var baseDamp = stabilizationLevel === 2 ? GAME_CONSTANTS.STABILIZATION_DAMP_FACTOR_FULL * 0.9 : GAME_CONSTANTS.STABILIZATION_DAMP_FACTOR_PARTIAL * 0.95; var dampFactor = Math.max(0.35, baseDamp - fruitLevel * GAME_CONSTANTS.STABILIZATION_DAMP_LEVEL_FACTOR * 1.1); fruit.vx *= dampFactor; fruit.vy *= dampFactor; fruit.angularVelocity *= dampFactor * 0.85; var stopThreshold = GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_LEVEL_FACTOR; // Unconditionally stop when fully stabilized (level 2) and below movement threshold if (stabilizationLevel === 2 && movementMagnitude < stopThreshold) { fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; fruit.isFullyStabilized = true; // Track frames of being fully stabilized if (!fruit._sleepCounter) fruit._sleepCounter = 0; // Only increment sleep counter if both linear and angular velocities are near zero // Require extremely low angular velocity (half of the threshold) for sleep qualification if (movementMagnitude < stopThreshold * 0.8 && Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.ANGULAR_STOP_THRESHOLD * 0.5) { fruit._sleepCounter++; // After being fully stabilized for SLEEP_DELAY_FRAMES consecutive frames, put to sleep if (fruit._sleepCounter >= GAME_CONSTANTS.SLEEP_DELAY_FRAMES) { fruit.isSleeping = true; fruit.isFullyStabilized = true; // Ensure fully stabilized state when sleeping } } else { // Reset sleep counter if either velocity isn't near zero fruit._sleepCounter = 0; fruit.isSleeping = false; // Ensure fruit is awake if conditions aren't met } return true; } } // If we reach here, fruit doesn't meet stabilization criteria fruit.isFullyStabilized = false; fruit._sleepCounter = 0; // Reset sleep counter when not stabilized fruit.isSleeping = false; // Ensure fruit is awake when not stabilized return false; }; self.apply = function (fruit) { if (fruit.isStatic || fruit.merging) return; // Skip physics calculations for sleeping fruits at the very start if (fruit.isSleeping) { // Keep fruit in the exact same position - no physics updates fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; return; } // Reset stabilization flag if significant external force applied (e.g., explosion) // Wake fruit up from potential stabilization if there's significant velocity change var velocityDeltaX = Math.abs(fruit.vx - fruit.lastVx); var velocityDeltaY = Math.abs(fruit.vy - fruit.lastVy); if ((fruit.isFullyStabilized || fruit._sleepCounter > 0) && (velocityDeltaX > 0.7 || velocityDeltaY > 0.7)) { fruit.isFullyStabilized = false; fruit._sleepCounter = 0; // Reset sleep counter on significant velocity change fruit.isSleeping = false; // Ensure fruit is awake on significant velocity change } // More aggressively ensure stability when at rest - use a tighter threshold if (fruit.isFullyStabilized && Math.abs(fruit.vx) < 0.3 && Math.abs(fruit.vy) < 0.3) { fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; // Ensure zero angular velocity return; } fruit.lastVx = fruit.vx; fruit.lastVy = fruit.vy; var fruitLevel = getFruitLevel(fruit); var isMidAir = !fruit._boundaryContacts || !fruit._boundaryContacts.floor && !fruit._boundaryContacts.left && !fruit._boundaryContacts.right; // Use consistent power exponential scaling for gravity var gravityMultiplier = 1 + Math.pow(fruitLevel, 1.8) * 0.35; fruit.vy += fruit.gravity * gravityMultiplier; var maxVelocity = GAME_CONSTANTS.MAX_VELOCITY_BASE - fruitLevel * GAME_CONSTANTS.MAX_VELOCITY_LEVEL_FACTOR; fruit.vx = Math.max(-maxVelocity, Math.min(maxVelocity, fruit.vx)); fruit.vy = Math.max(-maxVelocity, Math.min(maxVelocity, fruit.vy)); // Removed separate LEVEL_DOWNWARD_FORCE_FACTOR as it's now incorporated in the gravity calculation if (fruit.lastVx !== undefined && fruit.lastVy !== undefined) { var dvx = fruit.vx - fruit.lastVx; var dvy = fruit.vy - fruit.lastVy; // Make inertia scale consistently with fruitLevel var inertiaResistance = Math.min(0.98, Math.pow(fruitLevel, 2.0) * 0.015); fruit.vx = fruit.lastVx + dvx * (1 - inertiaResistance); fruit.vy = fruit.lastVy + dvy * (1 - inertiaResistance); } 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); // Apply strong damping when velocity is very low to help fruits stabilize faster if (movementMagnitude < 0.5) { fruit.vx *= 0.80; fruit.vy *= 0.80; fruit.angularVelocity *= 0.70; } var isFullyStabilizedByFunc = self.stabilizeFruit(fruit, movementMagnitude, velocityChange); fruit.isFullyStabilized = isFullyStabilizedByFunc; // Update fruit's state if (!fruit.isFullyStabilized) { // Only apply rolling rotation when in contact with a surface var isRolling = fruit._boundaryContacts && (fruit._boundaryContacts.floor || fruit._boundaryContacts.left || fruit._boundaryContacts.right); if (isRolling && (movementMagnitude > 0.5 || velocityChange > 0.3)) { var rotationFactor = 0.035 / (1 + fruitLevel * 0.08); var targetAngularVelocity = fruit.vx * rotationFactor; fruit.angularVelocity = fruit.angularVelocity * 0.4 + targetAngularVelocity * 0.6; } else { // Apply stronger damping when not rolling or in air fruit.angularVelocity *= isRolling ? 0.75 : 0.65; // More damping when not rolling } fruit.rotation += fruit.angularVelocity; // Make friction increase slightly with level (heavier objects have more surface interaction) var frictionModifier = 0.98 + fruitLevel * 0.005; // Apply more friction when nearly stopped to push toward stabilization var nearStopMultiplier = movementMagnitude < 1.0 ? 0.95 : 0.98; // More aggressive fruit.vx *= fruit.friction * frictionModifier * nearStopMultiplier; fruit.vy *= fruit.friction * frictionModifier * nearStopMultiplier * 0.98; // Apply very aggressive damping for extremely low velocities before stabilization if (!isMidAir && movementMagnitude < 0.2) { fruit.vx *= 0.5; // Very strong damping fruit.vy *= 0.5; fruit.angularVelocity *= 0.4; if (movementMagnitude < 0.05) { // Extremely low fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; } } var stopThreshold = GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_LEVEL_FACTOR; stopThreshold = Math.max(0.05, stopThreshold); if (Math.abs(fruit.vx) < stopThreshold) fruit.vx = 0; // Only stop vertical velocity if NOT in mid-air AND not fully stabilized by function if (Math.abs(fruit.vy) < stopThreshold && !isMidAir) fruit.vy = 0; self.handleRotationDamping(fruit); } fruit.x += fruit.vx; fruit.y += fruit.vy; if (isMidAir) { var currentMinFallSpeed = GAME_CONSTANTS.MIN_FALL_SPEED + fruitLevel * 0.02; if (fruit.vy < currentMinFallSpeed) { fruit.vy = currentMinFallSpeed; } fruit.isFullyStabilized = false; // Explicitly ensure mid-air is not stabilized } }; self.handleRotationDamping = function (fruit) { var movementMagnitude = Math.sqrt(fruit.vx * fruit.vx + fruit.vy * fruit.vy); var fruitLevel = getFruitLevel(fruit); var isMidAir = !fruit._boundaryContacts || !fruit._boundaryContacts.floor && !fruit._boundaryContacts.left && !fruit._boundaryContacts.right; // Scale rotation factor inversely with fruitLevel - higher level = less rotation from rolling var rotationFactor = 0.04 / (1 + fruitLevel * 0.15); // Increased inverse scaling with level var targetAngularVelocity = 0; // Initialize to 0, only set when rolling on floor // Only calculate rolling-based rotation when in contact with floor and has meaningful velocity if (fruit._boundaryContacts && fruit._boundaryContacts.floor && Math.abs(fruit.vx) > 0.2) { targetAngularVelocity = fruit.vx * rotationFactor; } var hasNeighbors = fruit.neighborContacts && fruit.neighborContacts.length > 0; var neighborCount = hasNeighbors ? fruit.neighborContacts.length : 0; // Only apply rotation forces if moving with sufficient velocity and targetAngularVelocity is not zero (i.e., rolling on floor) if (movementMagnitude > 0.3 && targetAngularVelocity !== 0) { // Limit the effect of very small vx values to prevent wild spinning var vxAmplitudeFactor = Math.min(Math.abs(fruit.vx) * 2, 1.0); // Less influence when more neighbors present - scaled by neighbor count var blendRatio = Math.max(0.2, 0.6 - neighborCount * 0.08) * vxAmplitudeFactor; // Only blend toward target if target is less than current, or if current is very low if (Math.abs(targetAngularVelocity) < Math.abs(fruit.angularVelocity) || Math.abs(fruit.angularVelocity) < 0.01) { fruit.angularVelocity = fruit.angularVelocity * (1 - blendRatio) + targetAngularVelocity * blendRatio; } // Correct rotation direction if it's opposite of movement var targetDirection = fruit.vx > 0 ? 1 : -1; var currentDirection = fruit.angularVelocity > 0 ? 1 : -1; if (targetDirection !== currentDirection && Math.abs(fruit.vx) > 0.5) { fruit.angularVelocity *= 0.25; // Stronger correction when rotation direction opposite of movement } } // Base damping primarily on movement magnitude and neighbor count var movementFactor = Math.min(1, movementMagnitude * 2); var neighborFactor = Math.min(1, neighborCount * 0.2); var levelFactor = fruitLevel * 0.06; // Increased level factor influence // Calculate rotation damp factor based on simplified continuous scaling var rotationDampFactor; var isRolling = fruit._boundaryContacts && fruit._boundaryContacts.floor && Math.abs(fruit.vx) > 0.2; // Base damping that scales uniformly with level var baseDamp = 0.6 - levelFactor * 0.4; // Adjust based on movement state (continuous scaling for all cases) if (movementMagnitude < 0.05) { // Near-zero movement gets extreme damping rotationDampFactor = 0.12 - levelFactor * 0.6; } else if (isMidAir) { // Mid-air damping rotationDampFactor = 0.5 - levelFactor * 0.8; } else if (!isRolling) { // Not rolling on floor rotationDampFactor = 0.5 - levelFactor * 0.7; } else if (movementMagnitude < 0.3) { // Slow movement - factor in neighborhood density continuously rotationDampFactor = 0.25 - levelFactor * 0.7 - neighborFactor * 0.15; } else if (hasNeighbors) { // With neighbors - continuous scaling based on neighbor count rotationDampFactor = baseDamp - neighborFactor * 0.1; } else { // Default case - moving freely rotationDampFactor = baseDamp; } // Check if fruit is rolling with continuous speed-based scaling var isRolling = !isMidAir && movementMagnitude > 0.5 && fruit._boundaryContacts && (fruit._boundaryContacts.floor || fruit._boundaryContacts.left || fruit._boundaryContacts.right); if (isRolling) { // Continuous damping based on movement speed - smoother transition var speedFactor = Math.min(1.0, movementMagnitude / 1.5); var baseRollingDamp = 0.7 - levelFactor * 0.4; var maxRollingDamp = 0.85 - levelFactor * 0.2; rotationDampFactor = baseRollingDamp + speedFactor * (maxRollingDamp - baseRollingDamp); } // Apply the calculated damping factor fruit.angularVelocity *= rotationDampFactor; // Apply additional damping when velocity changes suddenly if (fruit.lastVx !== undefined && Math.abs(fruit.vx - fruit.lastVx) > 0.5) { fruit.angularVelocity -= (fruit.vx - fruit.lastVx) * 0.02; } // Dramatically increase damping when velocity is very low if (movementMagnitude < 0.2) { // Apply extreme angular damping when linear movement is minimal var linearDampingFactor = movementMagnitude < 0.08 ? 0.03 : 0.15; // More aggressive damping fruit.angularVelocity *= linearDampingFactor; // Directly zero out tiny angular velocities when there's almost no movement if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.ANGULAR_STOP_THRESHOLD || movementMagnitude < 0.02) { // Using constant for threshold fruit.angularVelocity = 0; } } // Apply extreme damping at near-zero linear speed if (movementMagnitude < 0.1) { // Apply very aggressive angular damping factor for extremely low movement fruit.angularVelocity *= 0.05; } // Implement neighborhood-aware damping for "trapped" or nearly still fruits if (fruit.neighborContacts && fruit.neighborContacts.length >= 2 && movementMagnitude < 0.5) { // More neighbors = stronger damping factor (lower value) var neighborDampFactor = 0.1 + Math.max(0, 0.4 - fruit.neighborContacts.length * 0.05); fruit.angularVelocity *= neighborDampFactor; // Immediately stop rotation for fruits with many neighbors that are barely moving if (fruit.neighborContacts.length >= 4 && movementMagnitude < 0.2) { fruit.angularVelocity = 0; } } // Handling rolling physics when in contact with surfaces if (fruit._boundaryContacts) { // Apply rolling physics when in contact with floor or walls var isRolling = fruit._boundaryContacts.floor || fruit._boundaryContacts.left || fruit._boundaryContacts.right; if (isRolling && Math.abs(fruit.vx) > 0.2) { // Scale roll factor inversely with level - higher level fruits roll less var rollFactor = 0.018 / (1 + fruitLevel * 0.25); // Reduced roll factor to prevent wild spinning // Apply velocity scaling that tapers off for higher speeds var velocityScale = Math.min(Math.abs(fruit.vx), 5.0) / 5.0; var idealRollingVelocity = fruit.vx * rollFactor * Math.pow(velocityScale, 0.7); // More gentle blending that won't increase angularVelocity if it's already higher var currentMagnitude = Math.abs(fruit.angularVelocity); var targetMagnitude = Math.abs(idealRollingVelocity); // Only blend toward ideal if current angular velocity is too low or if we need to switch direction if (currentMagnitude < targetMagnitude || Math.sign(fruit.angularVelocity) != Math.sign(idealRollingVelocity)) { var blendRatio = 0.2; // Gentler blending ratio to prevent sudden kicks fruit.angularVelocity = fruit.angularVelocity * (1 - blendRatio) + idealRollingVelocity * blendRatio; } } else if (isMidAir) { // Apply stronger damping when airborne with no contacts fruit.angularVelocity *= 0.85; } } // Increase wall damping if (fruit._boundaryContacts && (fruit._boundaryContacts.left || fruit._boundaryContacts.right) && Math.abs(fruit.angularVelocity) > 0.01) { fruit.angularVelocity *= 0.6; // Even stronger wall damping } // Lowered level-based thresholds for stopping rotation - significantly lowered var angularThreshold = GAME_CONSTANTS.ANGULAR_STOP_THRESHOLD - fruitLevel * 0.0002; // Even lower threshold for higher level fruits var restFramesThreshold = Math.max(1, 1 - Math.floor(fruitLevel * 0.2)); // Reduced to just 1 frame for most levels if (Math.abs(fruit.angularVelocity) < angularThreshold) { fruit.rotationRestCounter++; if (fruit.rotationRestCounter > 1) { // Set to 1 for immediate stopping fruit.angularVelocity = 0; } } else { fruit.rotationRestCounter = 0; } // Continuous scaling for max angular velocity based on level with asymptotic limit var maxAngularMultiplier = 1.2 - Math.min(0.9, Math.pow(fruitLevel, 1.2) * 0.05); fruit.angularVelocity = Math.min(Math.max(fruit.angularVelocity, -self.maxAngularVelocity * maxAngularMultiplier), self.maxAngularVelocity * maxAngularMultiplier); }; 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 = GAME_CONSTANTS.SPATIAL_GRID_REBUILD_INTERVAL_MS; 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 = (Math.max(0, activeFruitLevel - 4) + Math.max(0, targetFruitLevel - 4)) * 2; var combinedRadii = activeFruitRadius + targetFruitRadius - hitboxReduction; var minDistanceSquared = combinedRadii * combinedRadii; return distanceSquared < minDistanceSquared; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xffe122 }); /**** * Game Code ****/ // --- Constants --- var _GAME_CONSTANTS; function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } var GAME_CONSTANTS = (_GAME_CONSTANTS = { GAME_WIDTH: 2048, GAME_HEIGHT: 2732, DROP_POINT_Y: 200, DROP_START_Y_OFFSET: 200, CLICK_DELAY_MS: 300, FRUIT_IMMUNITY_MS: 1000, MERGE_GRACE_MS: 2000, GAME_OVER_LINE_Y: 550, GAME_OVER_COUNTDOWN_MS: 3000, // Physics BASE_GRAVITY: 5.0, GRAVITY_LEVEL_MULTIPLIER: 0.4, // Adjusted gravity scaling FRICTION: 0.80, // Increased base friction ANGULAR_FRICTION: 0.70, GROUND_ANGULAR_FRICTION: 0.60, // Further increased ground friction MAX_ANGULAR_VELOCITY: 0.15, ELASTICITY_HIGH: 0.85, // Sleep state constants SLEEP_DELAY_FRAMES: 15, WAKE_UP_IMPULSE_THRESHOLD: 1.5, ANGULAR_STOP_THRESHOLD: 0.0005 }, _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_GAME_CONSTANTS, "SLEEP_DELAY_FRAMES", 15), "WAKE_UP_IMPULSE_THRESHOLD", 1.5), "ELASTICITY_LOW_START_LEVEL", 4), "ELASTICITY_LOW_BASE", 0.8), "ELASTICITY_DECREASE_FACTOR", 0.2 / 9), "MIN_FALL_SPEED", 0.1), "MAX_VELOCITY_BASE", 65), "MAX_VELOCITY_LEVEL_FACTOR", 5), "LEVEL_INERTIA_FACTOR", 0.05), "LEVEL_DOWNWARD_FORCE_FACTOR", 0.1), _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_GAME_CONSTANTS, "BOUNCE_SOUND_VELOCITY_THRESHOLD", 0.5), "COLLISION_SEPARATION_FACTOR", 1.01), "INTER_FRUIT_FRICTION", 0.2), "ROTATION_TRANSFER_FACTOR", 0.007), "STABILIZATION_MOVEMENT_THRESHOLD_BASE", 0.7), "STABILIZATION_MOVEMENT_THRESHOLD_LEVEL_FACTOR", 0.08), "STABILIZATION_ANGULAR_THRESHOLD_BASE", 0.07), "STABILIZATION_ANGULAR_THRESHOLD_LEVEL_FACTOR", 0.006), "STABILIZATION_SCORE_RATE_BASE", 1.5), "STABILIZATION_SCORE_LEVEL_FACTOR", 0.2), _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_GAME_CONSTANTS, "STABILIZATION_WALL_SCORE_RATE", 1.2), "STABILIZATION_SURROUNDED_SCORE_RATE", 1.5), "STABILIZATION_SCORE_LEVEL_INFLUENCE", 0.1), "STABILIZATION_THRESHOLD_FULL_BASE", 15), "STABILIZATION_THRESHOLD_FULL_LEVEL_FACTOR", 0.8), "STABILIZATION_THRESHOLD_PARTIAL_BASE", 8), "STABILIZATION_THRESHOLD_PARTIAL_LEVEL_FACTOR", 0.4), "STABILIZATION_DAMP_FACTOR_FULL", 0.6), "STABILIZATION_DAMP_FACTOR_PARTIAL", 0.85), "STABILIZATION_DAMP_LEVEL_FACTOR", 0.03), _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_GAME_CONSTANTS, "STABILIZATION_STOP_THRESHOLD_BASE", 0.15), "STABILIZATION_STOP_THRESHOLD_LEVEL_FACTOR", 0.01), "SPATIAL_GRID_REBUILD_INTERVAL_MS", 60000), "SPATIAL_GRID_CELL_SIZE_FACTOR", 1.1), "CHARGE_NEEDED_FOR_RELEASE", 15), "PORTAL_UI_Y", 120), "PORTAL_UI_X_OFFSET", 870), "PORTAL_TWEEN_DURATION", 300), "PORTAL_PULSE_DURATION", 500), "PINEAPPLE_MERGES_NEEDED", 15), _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_GAME_CONSTANTS, "PINEAPPLE_START_Y", 200), "PINEAPPLE_END_POS_FACTOR", 0.16), "PINEAPPLE_TWEEN_DURATION", 300), "PINEAPPLE_IMMUNITY_MS", 3000), "FIRE_BASE_COUNT", 3), "FIRE_FRUIT_TYPE_THRESHOLD", 1), "FIRE_MAX_COUNT", 15), "FIRE_START_Y_OFFSET", 50), "FIRE_STACK_Y_OFFSET", 100), "FIRE_X_SPREAD", 500), _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_GAME_CONSTANTS, "FIRE_FLICKER_SPEED_BASE", 500), "FIRE_FLICKER_SPEED_RANDOM", 300), "FIRE_ALPHA_MIN", 0.1), "FIRE_ALPHA_MAX", 0.5), "FIRE_FRAME_DURATION", 200), "COCONUT_SPAWN_SCORE_INTERVAL", 1000), "EVOLUTION_LINE_Y", 120), "EVOLUTION_LINE_X_OFFSET", 350), "EVOLUTION_ICON_MAX_SIZE", 150), "EVOLUTION_ICON_SPACING", 20), _defineProperty(_defineProperty(_GAME_CONSTANTS, "SCORE_TEXT_Y", 400), "SCORE_TEXT_SIZE", 120)); var gameOverLine; var pineapple; var pineappleActive = false; var pineapplePushCount = 0; var readyToReleaseCharged = false; var trajectoryLine; var isClickable = true; var evolutionLine; var fireContainer; var activeFireElements = []; var fruitLevels = { 'CHERRY': 1, 'GRAPE': 2, 'APPLE': 3, 'ORANGE': 4, 'WATERMELON': 5, 'PINEAPPLE': 6, 'MELON': 7, 'PEACH': 8, 'COCONUT': 9, 'DURIAN': 10 }; 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 = GAME_CONSTANTS.GAME_WIDTH; var gameHeight = GAME_CONSTANTS.GAME_HEIGHT; var fruits = []; var nextFruitType = null; var activeFruit = null; var wallLeft, wallRight, gameFloor; var dropPointY = GAME_CONSTANTS.DROP_POINT_Y; 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; var lastScoreCheckForCoconut = 0; 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 >= GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED && !pineappleActive && pineapple) { pineappleActive = true; pineapple.isStatic = false; pineapple.immuneToGameOver = true; applyDropPhysics(pineapple, 2.5); fruits.push(pineapple); if (spatialGrid) spatialGrid.insertObject(pineapple); LK.setTimeout(function () { if (pineapple && fruits.includes(pineapple)) { pineapple.immuneToGameOver = false; } }, GAME_CONSTANTS.PINEAPPLE_IMMUNITY_MS); setupPineapple(); mergeCounter = 0; } } 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 = GAME_CONSTANTS.GAME_OVER_LINE_Y; gameOverLine.scaleX = gameWidth / 100; // Scale to full screen width (Line is 100px wide by default) 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 + GAME_CONSTANTS.DROP_START_Y_OFFSET; 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; }, GAME_CONSTANTS.CLICK_DELAY_MS); activeFruit.isStatic = false; applyDropPhysics(activeFruit, 3.5); fruits.push(activeFruit); spatialGrid.insertObject(activeFruit); lastDroppedFruit = activeFruit; lastDroppedHasMerged = false; chargeCounter++; updateChargedBallDisplay(); if (chargeCounter >= GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE && !readyToReleaseCharged) { releaseChargedBalls(); } activeFruit.mergeGracePeriodActive = true; LK.setTimeout(function () { if (activeFruit && fruits.includes(activeFruit)) { activeFruit.mergeGracePeriodActive = false; } }, GAME_CONSTANTS.MERGE_GRACE_MS); 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 >= GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE) { 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 * 0.05); 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.angularVelocity = 0; // Explicitly reset angular velocity on drop fruit.gravity = GAME_CONSTANTS.BASE_GRAVITY * (1 + fruitLevel * GAME_CONSTANTS.GRAVITY_LEVEL_MULTIPLIER); fruit.safetyPeriod = false; fruit.immuneToGameOver = true; fruit.isFullyStabilized = false; // Reset stabilization state fruit.isSleeping = false; // Wake up the fruit when dropping fruit._sleepCounter = 0; // Reset sleep counter - ensure even if undefined LK.setTimeout(function () { if (fruit && fruits.includes(fruit)) { fruit.immuneToGameOver = false; } }, GAME_CONSTANTS.FRUIT_IMMUNITY_MS); } function updateScoreDisplay() { scoreText.setText(LK.getScore()); } function setupUI() { scoreText = new Text2("0", { size: GAME_CONSTANTS.SCORE_TEXT_SIZE, fill: 0x000000 }); scoreText.anchor.set(0.5, 0); LK.gui.top.addChild(scoreText); scoreText.y = GAME_CONSTANTS.SCORE_TEXT_Y; setupChargedBallDisplay(); } function setupChargedBallDisplay() { chargedBallUI = new ChargedBallUI(); // Initialization moved to class constructor 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 = []; // Reset surroundedFrames here as it's primarily determined by collisions fruits[k].surroundedFrames = 0; } } 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 = (Math.max(0, level1 - 4) + Math.max(0, level2 - 4)) * 2; // Use simplified reduction 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; } // Calculate precise overlap along the collision normal var overlap = combinedHalfWidths - absDistanceX; if (absDistanceY < combinedHalfHeights && overlap > 0) { // Use most accurate overlap calculation var verticalOverlap = combinedHalfHeights - absDistanceY; if (verticalOverlap < overlap) { overlap = verticalOverlap; } // Use exact normal vector from centers var normalizeX = dx / distance; var normalizeY = dy / distance; // Calculate proportional separation based on fruit mass var level1 = getFruitLevel(fruit1); var level2 = getFruitLevel(fruit2); // Use consistent exponential formula for mass based on level - using power of 3.5 var mass1 = Math.pow(level1, 3.5); var mass2 = Math.pow(level2, 3.5); var totalMass = mass1 + mass2; var moveRatio1 = totalMass > 0 ? mass2 / totalMass : 0.5; var moveRatio2 = totalMass > 0 ? mass1 / totalMass : 0.5; // Apply separation proportional to mass var moveX = overlap * normalizeX; var moveY = overlap * normalizeY; // Apply smaller separation factor for more stable stacking var separationFactor = GAME_CONSTANTS.COLLISION_SEPARATION_FACTOR; fruit1.x -= moveX * moveRatio1 * separationFactor; fruit1.y -= moveY * moveRatio1 * separationFactor; fruit2.x += moveX * moveRatio2 * separationFactor; fruit2.y += moveY * moveRatio2 * 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 consistent exponential mass formula - maintain same exponent as above var mass1 = Math.pow(level1, 3.5); var mass2 = Math.pow(level2, 3.5); var totalMass = mass1 + mass2; // Let impulse ratios be calculated directly from the exaggerated masses var impulseRatio1 = totalMass > 0 ? mass2 / totalMass : 0.5; var impulseRatio2 = totalMass > 0 ? mass1 / totalMass : 0.5; var velocityMagnitude = Math.sqrt(rvX * rvX + rvY * rvY); var velocityDampening = velocityMagnitude > 5 ? 0.5 + 2.5 / velocityMagnitude : 1.0; var impulse1 = impulse * impulseRatio1 * velocityDampening; var impulse2 = impulse * impulseRatio2 * velocityDampening; // Apply impulses with sleeping state awareness but no special level-based adjustments var impactThreshold = 1.2; // Threshold to determine significant impacts // For first fruit: apply impulse and wake up if needed // Wake up sleeping fruits on significant impacts based on WAKE_UP_IMPULSE_THRESHOLD if (fruit1.isSleeping) { if (Math.abs(impulse1) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD) { // Wake up if impact is very significant fruit1.isSleeping = false; fruit1.isFullyStabilized = false; fruit1._sleepCounter = 0; fruit1.vx -= impulse1 * normalizeX; fruit1.vy -= impulse1 * normalizeY; } else { // Keep sleeping but ensure proper state tracking fruit1.vx = 0; fruit1.vy = 0; fruit1.angularVelocity = 0; } } else if (fruit1.isFullyStabilized) { // Apply minimal impulse to stabilized fruits - just enough to handle edge cases if (Math.abs(impulse1) > impactThreshold) { // If impact is large, unstabilize and apply full impulse fruit1.isFullyStabilized = false; fruit1._sleepCounter = 0; // Reset sleep counter on destabilization fruit1.vx -= impulse1 * normalizeX; fruit1.vy -= impulse1 * normalizeY; } else { // For small impacts, apply minimal response that won't disturb stability fruit1.vx -= impulse1 * normalizeX * 0.1; fruit1.vy -= impulse1 * normalizeY * 0.1; } } else { fruit1.vx -= impulse1 * normalizeX; fruit1.vy -= impulse1 * normalizeY; } // For second fruit: apply impulse with the same sleep/stabilization logic if (fruit2.isSleeping) { if (Math.abs(impulse2) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD) { // Wake up if impact is very significant fruit2.isSleeping = false; fruit2.isFullyStabilized = false; fruit2._sleepCounter = 0; fruit2.vx += impulse2 * normalizeX; fruit2.vy += impulse2 * normalizeY; } else { // Keep sleeping but ensure proper state tracking fruit2.vx = 0; fruit2.vy = 0; fruit2.angularVelocity = 0; } } else if (fruit2.isFullyStabilized) { // Apply minimal impulse to stabilized fruits - just enough to handle edge cases if (Math.abs(impulse2) > impactThreshold) { // If impact is large, unstabilize and apply full impulse fruit2.isFullyStabilized = false; fruit2._sleepCounter = 0; // Reset sleep counter on destabilization fruit2.vx += impulse2 * normalizeX; fruit2.vy += impulse2 * normalizeY; } else { // For small impacts, apply minimal response that won't disturb stability fruit2.vx += impulse2 * normalizeX * 0.1; fruit2.vy += impulse2 * normalizeY * 0.1; } } else { fruit2.vx += impulse2 * normalizeX; fruit2.vy += impulse2 * normalizeY; } // Apply additional post-collision damping to dissipate energy more rapidly fruit1.vx *= 0.95; // Increased damping factor to reduce velocities after collision fruit1.vy *= 0.95; fruit2.vx *= 0.95; fruit2.vy *= 0.95; // Apply stronger damping for low-velocity collisions to promote settling if (velocityMagnitude < 3.0) { fruit1.vx *= 0.92; fruit1.vy *= 0.92; fruit2.vx *= 0.92; fruit2.vy *= 0.92; } var tangentX = -normalizeY; var tangentY = normalizeX; var tangentVelocity = rvX * tangentX + rvY * tangentY; // Increased friction factor from 0.1 to 0.2 var frictionImpulse = -tangentVelocity * 0.2; // Directly using 0.2 instead of GAME_CONSTANTS.INTER_FRUIT_FRICTION // Apply tangential friction with same stabilization logic if (!fruit1.isFullyStabilized) { fruit1.vx -= frictionImpulse * tangentX * impulseRatio1; fruit1.vy -= frictionImpulse * tangentY * impulseRatio1; } if (!fruit2.isFullyStabilized) { fruit2.vx += frictionImpulse * tangentX * impulseRatio2; fruit2.vy += frictionImpulse * tangentY * impulseRatio2; } // "Static Friction" effect when relative angular velocity is very low if (Math.abs(fruit1.angularVelocity - fruit2.angularVelocity) < 0.005) { // Apply stronger damping when angular velocities are nearly matched fruit1.angularVelocity *= 0.5; fruit2.angularVelocity *= 0.5; } var tangentialComponent = rvX * tangentX + rvY * tangentY; var inertia1 = mass1 * Math.pow(fruit1.width / 2, 2); var inertia2 = mass2 * Math.pow(fruit2.width / 2, 2); // Only apply angular impulse if the tangential component or contact velocity is significant var tangentialMagnitude = Math.abs(tangentialComponent); var contactMagnitude = Math.abs(contactVelocity); var angularThreshold = 1.8; // Further increase threshold for generating rotational force // Calculate angular impulse with adjustment for impact magnitude var angularImpulse = 0; if (tangentialMagnitude > angularThreshold || contactMagnitude > angularThreshold) { // Modified impact factor calculation to create a more pronounced curve // This makes small impacts produce almost no rotation while preserving rotation for significant impacts var baseImpact = Math.max(tangentialMagnitude, contactMagnitude); // Use quadratic curve for smoother progression var impactFactor = Math.min(1.0, Math.pow((baseImpact - angularThreshold) / 4.0, 2) * 3.0); // Prioritize tangential component - direct hits should cause less spin var directnessRatio = tangentialMagnitude / (tangentialMagnitude + contactMagnitude + 0.001); // Increase spin for glancing blows (mostly tangential), reduce for direct hits var tangentialBoost = Math.min(1.8, 1.0 + directnessRatio); angularImpulse = tangentialComponent * GAME_CONSTANTS.ROTATION_TRANSFER_FACTOR * impactFactor * tangentialBoost; } // Apply angular velocity with same stabilization logic if (!fruit1.isFullyStabilized && angularImpulse !== 0) { fruit1.angularVelocity += inertia2 > 0 ? angularImpulse * (inertia1 / (inertia1 + inertia2)) : angularImpulse * 0.5; fruit1.angularVelocity *= 0.85; // Stronger damping (changed from 0.95) } if (!fruit2.isFullyStabilized && angularImpulse !== 0) { fruit2.angularVelocity -= inertia1 > 0 ? angularImpulse * (inertia2 / (inertia1 + inertia2)) : angularImpulse * 0.5; fruit2.angularVelocity *= 0.85; // Stronger damping (changed from 0.95) } // Apply additional angular damping regardless of impact - helps settle all post-collision rotation fruit1.angularVelocity *= 0.85; fruit2.angularVelocity *= 0.85; // Apply stronger damping for low-velocity collisions if (velocityMagnitude < 2.0) { fruit1.angularVelocity *= 0.8; fruit2.angularVelocity *= 0.8; } // Only unstabilize on significantly large angular impacts if (Math.abs(angularImpulse) > 0.015) { // Unstabilize on significant angular impact if (fruit1.isFullyStabilized) fruit1.isFullyStabilized = false; if (fruit2.isFullyStabilized) fruit2.isFullyStabilized = false; } 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.surroundedFrames++; // Increment surrounded counter on collision fruit2.surroundedFrames++; 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); // Removed bounce flag dependent damping } } } } } } 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 = Math.max(0, fruitLevel - 4) * 2; 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; // Game over if fruit is overlapping line AND not moving significantly OR fully stabilized var stableOrSlowing = Math.abs(fruit.vy) < 1.0 || fruit.isFullyStabilized || fruit._boundaryContacts && (fruit._boundaryContacts.left || fruit._boundaryContacts.right || fruit._boundaryContacts.floor); if (stableOrSlowing) { if (!fruit.gameOverTimer) { fruit.gameOverTimer = Date.now(); tween(fruit, { alpha: 0.5 }, { duration: 500, easing: tween.easeInOut, onFinish: function onFinish() { tween(fruit, { alpha: 1 }, { duration: 500, easing: tween.easeInOut, repeat: 2 }); } }); continue; } var currentTime = Date.now(); if (currentTime - fruit.gameOverTimer >= GAME_CONSTANTS.GAME_OVER_COUNTDOWN_MS) { gameOver = true; LK.showGameOver(); return; } } else { // Reset timer if fruit starts moving significantly while overlapping fruit.gameOverTimer = null; fruit.alpha = 1; } } else { // Reset timer if no longer overlapping horizontally if (fruit.gameOverTimer) { fruit.gameOverTimer = null; fruit.alpha = 1; } } } else { fruit.safetyPeriod = undefined; if (fruit.gameOverTimer) { fruit.gameOverTimer = null; fruit.alpha = 1; } } } } function setupPineapple() { pineapple = new Fruit(FruitTypes.PINEAPPLE); pineapple.x = -pineapple.width / 2; pineapple.y = GAME_CONSTANTS.PINEAPPLE_START_Y; pineapple.isStatic = true; pineappleActive = false; pineapplePushCount = 0; game.addChild(pineapple); } function pushPineapple() { if (!pineappleActive && pineapple) { var step = mergeCounter; var totalSteps = GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED; var percentage = Math.min(step / totalSteps, 1.0); var startPos = -pineapple.width / 2; var endPos = gameWidth * GAME_CONSTANTS.PINEAPPLE_END_POS_FACTOR; var newX = startPos + percentage * (endPos - startPos); tween(pineapple, { x: newX }, { duration: GAME_CONSTANTS.PINEAPPLE_TWEEN_DURATION, easing: tween.bounceOut }); } } function initGame() { LK.setScore(0); gameOver = false; if (fruits && fruits.length > 0) { // Ensure cleanup only happens if needed for (var i = fruits.length - 1; i >= 0; i--) { if (fruits[i]) removeFruitFromGame(fruits[i]); } } fruits = []; chargeCounter = 0; if (chargedBallUI) chargedBallUI.destroy(); chargedBallUI = null; readyToReleaseCharged = false; lastScoreCheckForCoconut = 0; lastDroppedFruit = null; lastDroppedHasMerged = false; mergeCounter = 0; isClickable = true; if (fireContainer) { for (var i = activeFireElements.length - 1; i >= 0; i--) { if (activeFireElements[i]) activeFireElements[i].destroy(); } fireContainer.destroy(); } fireContainer = new Container(); game.addChildAt(fireContainer, 0); 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 * GAME_CONSTANTS.SPATIAL_GRID_CELL_SIZE_FACTOR); spatialGrid = new SpatialGrid(optimalCellSize); } // Destroy existing boundaries before setup 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(); LK.playMusic('bgmusic'); // Start music after potential cleanup setupBoundaries(); setupUI(); setupPineapple(); updateFireBackground(); trajectoryLine = game.addChild(new TrajectoryLine()); trajectoryLine.createDots(); evolutionLine = game.addChild(new EvolutionLine()); evolutionLine.initialize(); updateScoreDisplay(); activeFruit = null; createNextFruit(); 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; 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); } }); } 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 + GAME_CONSTANTS.DROP_START_Y_OFFSET; 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); } // First phase: Apply gravity and update velocities for (var i = fruits.length - 1; i >= 0; i--) { var fruit = fruits[i]; if (!fruit || fruit.isStatic || fruit.merging) continue; // Apply physics without position update (updatePhysics handles this) fruit.updatePhysics(); } // Second phase: Check collisions and resolve overlaps checkFruitCollisions(); // Third phase: Apply boundary collisions and finalize positions for (var i = fruits.length - 1; i >= 0; i--) { var fruit = fruits[i]; if (!fruit || fruit.isStatic || fruit.merging) continue; var walls = { left: wallLeft, right: wallRight }; fruit.checkBoundaries(walls, gameFloor); // Final stabilization check - aggressively zero out extremely small velocity values if (fruit.isFullyStabilized) { // If fully stabilized, ensure complete zero velocity to prevent micro-movements fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; } else if (!fruit.isSleeping && fruit._boundaryContacts && (fruit._boundaryContacts.floor || fruit._boundaryContacts.left || fruit._boundaryContacts.right)) { // For fruits touching boundaries but not fully stabilized, apply additional damping // to encourage them to come to a full stop more quickly if (Math.abs(fruit.vx) < 0.2) fruit.vx = 0; if (Math.abs(fruit.vy) < 0.2) fruit.vy = 0; if (Math.abs(fruit.angularVelocity) < 0.01) fruit.angularVelocity = 0; } } // Fourth phase: Handle environmental interactions and position adjustments 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; } } // Final position update in the spatial grid spatialGrid.updateObject(fruit); } } } game.update = function () { if (gameOver) return; var currentScore = LK.getScore(); if (currentScore >= lastScoreCheckForCoconut + GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) { lastScoreCheckForCoconut = Math.floor(currentScore / GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) * GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL; spawnCoconut(); } else { if (currentScore > lastScoreCheckForCoconut) { lastScoreCheckForCoconut = Math.floor(currentScore / GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) * GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL; } } updatePhysics(); checkGameOver(); updateFireBackground(); for (var i = 0; i < activeFireElements.length; i++) { if (activeFireElements[i]) activeFireElements[i].update(); } }; initGame(); function updateFireBackground() { 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; } } var uniqueTypeCount = Object.keys(uniqueFruitTypes).length; // Updated fire count logic based on unique types present var targetFireCount = GAME_CONSTANTS.FIRE_BASE_COUNT + Math.floor(uniqueTypeCount / GAME_CONSTANTS.FIRE_FRUIT_TYPE_THRESHOLD); targetFireCount = Math.min(targetFireCount, GAME_CONSTANTS.FIRE_MAX_COUNT); if (targetFireCount > activeFireElements.length) { var newFiresCount = targetFireCount - activeFireElements.length; for (var i = 0; i < newFiresCount; i++) { createFireElement(activeFireElements.length); } } else if (targetFireCount < activeFireElements.length) { var fireToRemoveCount = activeFireElements.length - targetFireCount; for (var i = 0; i < fireToRemoveCount; i++) { var fire = activeFireElements.pop(); if (fire) { fire.destroy(); fireContainer.removeChild(fire); } } } } function createFireElement(index) { var yPos = gameHeight + GAME_CONSTANTS.FIRE_START_Y_OFFSET - index * GAME_CONSTANTS.FIRE_STACK_Y_OFFSET; var xPos = gameWidth / 2 + (Math.random() * GAME_CONSTANTS.FIRE_X_SPREAD - GAME_CONSTANTS.FIRE_X_SPREAD / 2); var newFire = new FireElement(xPos, yPos); if (index % 2 === 1) { newFire.fireAsset.scaleX = -1; } fireContainer.addChildAt(newFire, 0); activeFireElements.push(newFire); } function removeFruitFromGame(fruit) { // Before removing, wake up any sleeping neighbors that were resting on this fruit if (fruit.neighborContacts && fruit.neighborContacts.length > 0) { for (var i = 0; i < fruit.neighborContacts.length; i++) { var neighborId = fruit.neighborContacts[i]; for (var j = 0; j < fruits.length; j++) { if (fruits[j] && fruits[j].id === neighborId) { // Wake up any sleeping neighbors if (fruits[j].isSleeping || fruits[j].isFullyStabilized) { fruits[j].isSleeping = false; fruits[j].isFullyStabilized = false; fruits[j]._sleepCounter = 0; // Always reset sleep counter (don't check if exists) // Apply a small impulse to ensure physics recalculation fruits[j].vx += Math.random() * 0.6 - 0.3; fruits[j].vy -= 0.5; // Small upward force } } } } } var index = fruits.indexOf(fruit); if (index !== -1) fruits.splice(index, 1); spatialGrid.removeObject(fruit); fruit.destroy(); }
===================================================================
--- original.js
+++ change.js
@@ -685,9 +685,10 @@
fruit.isFullyStabilized = true;
// Track frames of being fully stabilized
if (!fruit._sleepCounter) fruit._sleepCounter = 0;
// Only increment sleep counter if both linear and angular velocities are near zero
- if (movementMagnitude < stopThreshold * 0.8 && Math.abs(fruit.angularVelocity) < angularThreshold * 0.7) {
+ // Require extremely low angular velocity (half of the threshold) for sleep qualification
+ if (movementMagnitude < stopThreshold * 0.8 && Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.ANGULAR_STOP_THRESHOLD * 0.5) {
fruit._sleepCounter++;
// After being fully stabilized for SLEEP_DELAY_FRAMES consecutive frames, put to sleep
if (fruit._sleepCounter >= GAME_CONSTANTS.SLEEP_DELAY_FRAMES) {
fruit.isSleeping = true;