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.
User prompt
Refactor checkFruitCollisions for Weight: Mass Calculation: Action: Use a strongly exponential formula for mass based on level. Hint: mass = Math.pow(fruitLevel, 2.5) or even Math.pow(fruitLevel, 3.0). This is critical for weight differences. Impulse Application: Action: Ensure impulseRatio1 and impulseRatio2 are calculated directly from these exaggerated masses. Action: Remove any additional explicit upward force multipliers based on level difference (e.g., levelDiffBonus, upwardForce). Let the mass difference inherent in the impulse ratios handle the interaction naturally. The greatly increased mass difference will make small fruits bounce off large ones effectively. Action: Remove the specific horizontal resistance logic for large fruits. The increased inertia from PhysicsComponent.apply should handle this. Action: Keep the logic to wake up sleeping/stabilized fruits if the impulse exceeds GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD.
User prompt
Rotation (handleRotationDamping): Action: Remove all explicit checks like if (isSmallFruit) or if (isLargeFruit). Action: Ensure the rotationFactor for targetAngularVelocity scales inversely with fruitLevel (higher level = less rotation from rolling). Action: Base the rotationDampFactor primarily on movementMagnitude and neighbor count. Dramatically increase damping when movementMagnitude is near zero. Action: Lower the angularThreshold and restFramesThreshold significantly to force rotation to stop quickly when angular velocity is low.
User prompt
align the evolution icons tothe middle of the screen, as they are now offset to the right
User prompt
stretch the game over line over the entire width of the screen
User prompt
Sleep State (isSleeping flag): Action: Keep the sleep state logic implemented previously. Ensure the condition to enter sleep requires zero (or near-zero) linear and angular velocity for GAME_CONSTANTS.SLEEP_DELAY_FRAMES. Action: Ensure the wakeUp conditions in collisions and removeFruitFromGame correctly reset isSleeping, isFullyStabilized, and _sleepCounter.
User prompt
Rotation (handleRotationDamping): Action: Remove all explicit checks like if (isSmallFruit) or if (isLargeFruit). Action: Ensure the rotationFactor for targetAngularVelocity scales inversely with fruitLevel (higher level = less rotation from rolling). Action: Base the rotationDampFactor primarily on movementMagnitude and neighbor count. Dramatically increase damping when movementMagnitude is near zero. Action: Lower the angularThreshold and restFramesThreshold significantly to force rotation to stop quickly when angular velocity is low.
User prompt
Action: Retain the check if (isMidAir) return false; at the beginning. Mid-air fruits MUST NOT stabilize. Action: Remove all explicit checks like if (isSmallFruit) or if (isLargeFruit). All thresholds and damping factors should now use formulas based only on fruitLevel. Action: Tune the stabilization thresholds. Make it easier for higher-level fruits to stabilize (lower thresholds) and slightly harder for lower-level fruits. Hint: fullStabilizationThreshold = BASE - fruitLevel * FACTOR. Ensure the FACTOR makes sense across levels 1-10. Action: Tune the damping factors. Make damping stronger for higher-level fruits during stabilization. Hint: dampFactor = Math.max(MIN_DAMP, BASE_DAMP - fruitLevel * FACTOR). Ensure the FACTOR makes sense. Action: Ensure the condition stabilizationLevel === 2 && movementMagnitude < stopThreshold unconditionally sets velocities and angular velocity to zero (for non-mid-air fruits).
User prompt
Refactor PhysicsComponent for Weight & Stability: Gravity Scaling: Action: Significantly increase the effect of fruitLevel on gravity. Modify the gravity calculation in PhysicsComponent.apply to use a higher power or multiplier. Hint: Change gravityMultiplier = 1 + fruitLevel * CONSTANT to something like gravityMultiplier = 1 + Math.pow(fruitLevel, 1.5) * HEAVIER_GRAVITY_FACTOR. Tune the HEAVIER_GRAVITY_FACTOR. Remove the separate LEVEL_DOWNWARD_FORCE_FACTOR. Reasoning: Makes the downward pull much stronger for higher-level fruits, directly impacting their tendency to fall and settle low. Inertia / Mass Scaling: Action: Modify the inertia calculation in PhysicsComponent.apply. Make inertiaResistance scale more dramatically with fruitLevel, potentially using a power function. Hint: Change inertiaResistance = Math.min(0.9, fruitLevel * CONSTANT) to inertiaResistance = Math.min(0.95, Math.pow(fruitLevel, 1.8) * HEAVIER_INERTIA_FACTOR). Tune the exponent and factor. Reasoning: Higher-level fruits will strongly resist changes in velocity, making them feel much heavier and harder to push around by smaller forces or fruits. Friction & Damping: Action: Increase base friction slightly (GAME_CONSTANTS.FRICTION closer to 0.85-0.9). Action: Make friction increase slightly with level (heavier objects might have slightly more surface interaction). Alternatively, keep friction uniform and rely solely on inertia. Test both approaches. Hint (If scaling): currentFriction = BASE_FRICTION * (1 - fruitLevel * 0.005) (Lower value = more friction). Action: Apply strong damping when velocity is very low. Inside apply, before the stabilization check, add: // If moving very slowly, apply strong damping to push towards zero if (movementMagnitude < 0.5) { // Tune this threshold fruit.vx *= 0.80; fruit.vy *= 0.80; fruit.angularVelocity *= 0.70; } Use code with caution. JavaScript Reasoning: Increased friction and aggressive low-velocity damping help bleed off the tiny movements that prevent halting.
User prompt
Remove Conflicting Logic: Review applyDropPhysics: Ensure no initial angularVelocity is being set here unless intended (e.g., a slight random spin on drop). Search for Other angularVelocity Modifiers: Check the entire codebase for any other places where fruit.angularVelocity might be getting modified unintentionally or based on outdated logic.
User prompt
Stabilization Check: The stabilizeFruit function already checks angularVelocity against a threshold. Ensure this threshold (angularThreshold) is appropriately low so that stabilization only occurs when the fruit has genuinely stopped rotating. Sleep Condition: When checking if a fruit should go to sleep (fruit.isSleeping = true), ensure the condition requires both near-zero linear velocity and near-zero angular velocity for the required number of frames. Action: Modify the sleep check within PhysicsComponent.stabilizeFruit (or wherever it's implemented) to include && Math.abs(fruit.angularVelocity) < angularStopThreshold. Reasoning: A fruit shouldn't sleep if it's still visibly spinning.
User prompt
Review Angular Impulse: Ensure the angular impulse calculated from collisions is reasonable and primarily occurs on significant impacts, not minor touches. Action: Double-check the rotationTransferFactor and the calculation using tangentialComponent. Ensure it's not generating excessive spin on minor contacts. Potentially add a condition so angular impulse is only applied if the contactVelocity or tangentialComponent is above a certain threshold. Reasoning: Prevents tiny collisions from inducing persistent, unrealistic spinning. Spin should mostly come from noticeable hits.
User prompt
Stronger Damping When Not Rolling: Apply significantly stronger angular damping when the fruit is not in rolling contact. Action: Inside handleRotationDamping, check if the fruit is not in contact with the floor (or another condition you define for rolling). If not rolling, multiply fruit.angularVelocity by a much smaller damping factor (e.g., 0.6 - 0.8) compared to when it is rolling. Reasoning: Spin should dissipate quickly in the air or when sliding without rolling friction. Linear Velocity Dependency: Make angular damping heavily dependent on the fruit's linear speed. Action: Modify the rotationDampFactor calculation. If movementMagnitude is very low (below the stabilization movement threshold), apply extremely high damping (e.g., multiply angularVelocity by 0.1 or even set it directly to 0 if below an angular threshold). Gradually decrease the damping strength as movementMagnitude increases. Reasoning: If the fruit isn't moving linearly, it shouldn't be rotating (unless just impacted). Aggressive Angular Stop Threshold: Lower the threshold for completely stopping rotation. Action: Reduce the angularThreshold used to check if angularVelocity is small enough to stop completely (e.g., from 0.008/0.012 down to 0.003/0.005). Action: Reduce the restFramesThreshold required to confirm the stop (e.g., from 4/6 frames down to 2/3 frames). Reasoning: Makes the fruit stop spinning sooner once its angular velocity is low.
User prompt
Modify the physics so that fruit rotation is primarily driven by rolling contact with surfaces or impacts from collisions. Rotation should cease quickly and reliably when linear movement stops or the fruit is airborne without recent impact. I. Refine Angular Velocity Calculation in PhysicsComponent.apply: Conditional Rolling Rotation: The current logic (targetAngularVelocity = fruit.vx * rotationFactor) assumes rotation is always proportional to horizontal velocity. This should only apply when rolling. Action: Calculate the targetAngularVelocity based on fruit.vx only if the fruit is determined to be in contact with the floor (fruit._boundaryContacts.floor) or potentially another resting fruit (this is harder to detect reliably, so start with just floor contact). Action: When the fruit is not rolling (i.e., mid-air and not recently hit, or sliding without rolling), do not calculate or apply this targetAngularVelocity based on vx. Instead, rely solely on damping and collision impulses. Action: Modify the blending logic. Instead of always blending towards the target (fruit.angularVelocity * 0.4 + targetAngularVelocity * 0.6), only apply this blend if the condition for rolling is met. Otherwise, just apply damping. Reasoning: Prevents fruits from gaining rotation just by moving horizontally in the air. Rotation should primarily occur due to surface interaction (rolling) or impacts.
User prompt
Make it easier for fruits to meet the stabilization criteria. Lower the STABILIZATION_THRESHOLD_FULL/PARTIAL constants slightly. Increase the STABILIZATION_SCORE_RATE constants slightly. Reasoning: Allows fruits to qualify for stabilization more quickly when movement is minimal.
User prompt
Implement Sleep State: Action: Add isSleeping (boolean) and _sleepCounter (number) properties to the Fruit class, initialized to false and 0. Action: In PhysicsComponent.stabilizeFruit, when a fruit is determined to be fully stabilized (meets the criteria for stabilizationLevel = 2 and minimal movement), increment its _sleepCounter. If the counter exceeds a threshold (e.g., GAME_CONSTANTS.SLEEP_DELAY_FRAMES), set fruit.isSleeping = true. Action: At the very beginning of PhysicsComponent.apply, add a check: if fruit.isSleeping is true, immediately return to skip all physics calculations for that frame. Action: Implement wake-up conditions: In checkFruitCollisions, if a sleeping fruit (fruit1.isSleeping or fruit2.isSleeping) is involved in a collision where the calculated impulse (impulse1 or impulse2) exceeds a threshold (GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD), set its isSleeping = false, isFullyStabilized = false, and reset _sleepCounter = 0. In removeFruitFromGame, before removing a fruit, iterate through its neighbors (using fruit.neighborContacts). If any neighbor isSleeping, wake it up (set isSleeping = false, isFullyStabilized = false, reset _sleepCounter). Optionally apply a tiny random impulse to the woken neighbor to ensure physics recalculation. Reasoning: This is the most robust way to stop micro-movements. Sleeping fruits consume minimal resources and won't move until significantly disturbed.
User prompt
Implement Sleep State: Action: Add isSleeping (boolean) and _sleepCounter (number) properties to the Fruit class, initialized to false and 0. Action: In PhysicsComponent.stabilizeFruit, when a fruit is determined to be fully stabilized (meets the criteria for stabilizationLevel = 2 and minimal movement), increment its _sleepCounter. If the counter exceeds a threshold (e.g., GAME_CONSTANTS.SLEEP_DELAY_FRAMES), set fruit.isSleeping = true. Action: At the very beginning of PhysicsComponent.apply, add a check: if fruit.isSleeping is true, immediately return to skip all physics calculations for that frame. Action: Implement wake-up conditions: In checkFruitCollisions, if a sleeping fruit (fruit1.isSleeping or fruit2.isSleeping) is involved in a collision where the calculated impulse (impulse1 or impulse2) exceeds a threshold (GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD), set its isSleeping = false, isFullyStabilized = false, and reset _sleepCounter = 0. In removeFruitFromGame, before removing a fruit, iterate through its neighbors (using fruit.neighborContacts). If any neighbor isSleeping, wake it up (set isSleeping = false, isFullyStabilized = false, reset _sleepCounter). Optionally apply a tiny random impulse to the woken neighbor to ensure physics recalculation. Reasoning: This is the most robust way to stop micro-movements. Sleeping fruits consume minimal resources and won't move until significantly disturbed.
User prompt
Please fix the bug: 'FruitTypes is not defined' in or related to this line: 'for (var fruitType in FruitTypes) {' Line Number: 1712
Code edit (1 edits merged)
Please save this source code
User prompt
Order of Operations (updatePhysics function): Verify Order: Confirm the general order: Apply forces (gravity) -> Update Velocities -> Check Collisions (resolve overlaps & apply impulses/friction) -> Apply Damping/Friction -> Check Stabilization/Stopping -> Update Position. Consider Damping Timing: Experiment with applying the main friction after stabilization checks vs. before. Applying it after might help zero out velocities more definitively if stabilization leaves tiny residual values.
User prompt
Review Collision Resolution (checkFruitCollisions): Separation Force: Ensure the separation force (moveX, moveY) applied when fruits overlap isn't too strong, which could perpetually push fruits apart and prevent them from settling snugly. Action: Check the separationFactor and potentially reduce it slightly if jittering is observed between touching, resting fruits. Overlap Calculation: Ensure the overlap calculation is accurate and doesn't lead to excessive separation.
User prompt
Fine-tune Friction and Damping (PhysicsComponent & CollisionComponent): Increase Base Friction: The global friction values might be too low, allowing small movements to persist. Action: Slightly decrease the GAME_CONSTANTS.FRICTION value (e.g., from 0.92 to 0.90 or 0.88). Action: Slightly decrease GAME_CONSTANTS.ANGULAR_FRICTION and GAME_CONSTANTS.GROUND_ANGULAR_FRICTION (e.g., from 0.85/0.6 to 0.8/0.5). Reasoning: Higher friction removes velocity more quickly overall. Increase Collision Damping: Add slight velocity damping directly after collision impulses are applied in checkFruitCollisions. Action: After applying impulse1, impulse2, etc., multiply both fruit1.vx/vy and fruit2.vx/vy by a small damping factor (e.g., 0.98 or 0.97). Reasoning: Helps dissipate energy introduced by the collision impulse more rapidly.
User prompt
Introduce Explicit "Sleep" State (Advanced Option): Concept: Add a boolean flag to the Fruit class, e.g., isSleeping. When a fruit becomes fully stabilized and remains so for a short duration (e.g., 10-15 frames), set isSleeping = true. Physics Skip: At the very beginning of PhysicsComponent.apply, check if fruit.isSleeping. If true, skip almost all physics calculations for that fruit (gravity, friction, stabilization checks). It should still participate in collision detection. Wake-Up Condition: In checkFruitCollisions, if a sleeping fruit is involved in a collision that applies a significant impulse (above a certain threshold), set its isSleeping = false so it recalculates physics normally again. Also, wake it up if the fruit it's resting on is removed. Reasoning: This completely removes micro-calculations for resting fruits, guaranteeing they stop and potentially improving performance. It requires careful implementation of the wake-up conditions.
User prompt
Goal: Modify the physics system so that fruits naturally come to a complete stop (zero velocity and angular velocity) when forces acting on them are negligible or balanced, preventing perpetual micro-movements or jittering. I. Refine Stabilization Logic (PhysicsComponent.stabilizeFruit): More Assertive Stopping Condition: The current stabilization might dampen velocities but not zero them out reliably enough. Action: Inside the stabilizeFruit function, when stabilizationLevel reaches 2 (full stabilization) AND the movementMagnitude is below the stopThreshold, unconditionally set fruit.vx = 0, fruit.vy = 0, and fruit.angularVelocity = 0. Crucially: Ensure this zeroing only happens if the fruit is not determined to be isMidAir at the start of the function. Mid-air fruits must never have their velocity zeroed by stabilization. Reasoning: This creates a definitive "stop" state that stabilization can achieve, rather than just approaching zero velocity asymptotically. Threshold Tuning: The thresholds determining when stabilization kicks in might be too high, or the score accumulation too slow. Action: Experiment with slightly lowering the fullStabilizationThreshold and partialStabilizationThreshold values (or the calculations based on fruitLevel) within stabilizeFruit. Action: Consider slightly increasing the rates at which stabilization metrics accumulate (stabilizationRate, wall/surrounded bonuses). Reasoning: This makes fruits qualify for stabilization sooner when their movement is small. Damping Factor Tuning: The damping applied during stabilization might be too weak. Action: Experiment with slightly decreasing the calculated dampFactor values (making them closer to 0, e.g., changing base values from 0.6/0.85 to 0.5/0.8) within stabilizeFruit. Reasoning: This removes energy (velocity) more quickly once stabilization starts.
Code edit (1 edits merged)
Please save this source code
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var ChargedBallUI = Container.expand(function () { var self = Container.call(this); self.chargeNeededForRelease = GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE; self.currentCharge = 0; self.isReadyToRelease = false; self.pulseAnimationActive = false; self.initialize = function () { self.portalAsset = self.attachAsset('portal', { anchorX: 0.5, anchorY: 0.5 }); self.portalAsset.x = GAME_CONSTANTS.GAME_WIDTH / 2 + GAME_CONSTANTS.PORTAL_UI_X_OFFSET; self.portalAsset.alpha = 0; self.portalAsset.scaleX = 0; self.portalAsset.scaleY = 0; self.y = GAME_CONSTANTS.PORTAL_UI_Y; }; self.updateChargeDisplay = function (chargeCount) { self.currentCharge = chargeCount; var remainingCount = Math.max(0, self.chargeNeededForRelease - self.currentCharge); var progressPercent = (self.chargeNeededForRelease - remainingCount) / self.chargeNeededForRelease; var targetScale = progressPercent; if (progressPercent > 0 && self.portalAsset.alpha === 0) { self.portalAsset.alpha = 1; } tween(self.portalAsset, { scaleX: targetScale, scaleY: targetScale, alpha: progressPercent }, { duration: GAME_CONSTANTS.PORTAL_TWEEN_DURATION, easing: tween.easeOut }); if (remainingCount === 0 && !self.isReadyToRelease) { self.setReadyState(true); } }; self.setReadyState = function (isReady) { self.isReadyToRelease = isReady; if (isReady) { tween(self.portalAsset, { scaleX: 1.0, scaleY: 1.0, alpha: 1.0, rotation: Math.PI * 2 }, { duration: GAME_CONSTANTS.PORTAL_TWEEN_DURATION, easing: tween.easeOut }); self.startPulseAnimation(); } }; self.startPulseAnimation = function () { if (self.pulseAnimationActive) return; self.pulseAnimationActive = true; self._pulseText(); }; self._pulseText = function () { if (!self.isReadyToRelease) { self.pulseAnimationActive = false; return; } tween(self.portalAsset, { scaleX: 1.3, scaleY: 1.3 }, { duration: GAME_CONSTANTS.PORTAL_PULSE_DURATION, easing: tween.easeInOut, onFinish: function onFinish() { if (!self.isReadyToRelease) { self.pulseAnimationActive = false; return; } tween(self.portalAsset, { scaleX: 1.0, scaleY: 1.0 }, { duration: GAME_CONSTANTS.PORTAL_PULSE_DURATION, easing: tween.easeInOut, onFinish: self._pulseText }); } }); }; self.reset = function () { self.isReadyToRelease = false; self.currentCharge = 0; self.pulseAnimationActive = false; tween(self.portalAsset, { alpha: 0 }, { duration: 200, easing: tween.easeOut }); tween(self.portalAsset, { scaleX: 0, scaleY: 0 }, { duration: 200, easing: tween.easeOut }); }; self.initialize(); return self; }); var CollisionComponent = Container.expand(function () { var self = Container.call(this); 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 + GAME_CONSTANTS.EVOLUTION_LINE_X_OFFSET; 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 + (currentLevel - 1) * GAME_CONSTANTS.GRAVITY_LEVEL_MULTIPLIER); self.friction = GAME_CONSTANTS.FRICTION; self.rotationRestCounter = physics.rotationRestCounter; self.maxAngularVelocity = GAME_CONSTANTS.MAX_ANGULAR_VELOCITY; self.isStatic = physics.isStatic; self.elasticity = currentLevel < GAME_CONSTANTS.ELASTICITY_LOW_START_LEVEL ? GAME_CONSTANTS.ELASTICITY_HIGH : GAME_CONSTANTS.ELASTICITY_LOW_BASE - (currentLevel - (GAME_CONSTANTS.ELASTICITY_LOW_START_LEVEL - 1)) * GAME_CONSTANTS.ELASTICITY_DECREASE_FACTOR; self.elasticity = Math.max(0.1, self.elasticity); // Ensure minimum elasticity self.wallContactFrames = collision.wallContactFrames; self.merging = mergeHandler.merging; self.mergeGracePeriodActive = mergeHandler.mergeGracePeriodActive; self.fromChargedRelease = mergeHandler.fromChargedRelease; self.safetyPeriod = false; self.immuneToGameOver = false; self.hasBounced = 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 return false; } var stabilizationLevel = 0; var fruitLevel = getFruitLevel(fruit); if (!fruit._stabilizeMetrics) { fruit._stabilizeMetrics = { consecutiveSmallMovements: 0, wallContactDuration: 0, surroundedDuration: 0, restingDuration: 0 }; } var movementThreshold = GAME_CONSTANTS.STABILIZATION_MOVEMENT_THRESHOLD_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_MOVEMENT_THRESHOLD_LEVEL_FACTOR * 1.2; // Lower threshold var angularThreshold = GAME_CONSTANTS.STABILIZATION_ANGULAR_THRESHOLD_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_ANGULAR_THRESHOLD_LEVEL_FACTOR * 1.2; // Lower threshold 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; // Increase rate even more fruit._stabilizeMetrics.consecutiveSmallMovements += stabilizationRate; fruit._stabilizeMetrics.restingDuration += stabilizationRate; } else { fruit._stabilizeMetrics.consecutiveSmallMovements = 0; fruit._stabilizeMetrics.restingDuration = Math.max(0, fruit._stabilizeMetrics.restingDuration - 2); } if (fruit.wallContactFrames > 0) fruit._stabilizeMetrics.wallContactDuration += GAME_CONSTANTS.STABILIZATION_WALL_SCORE_RATE * 1.4 + fruitLevel * 0.15; // Further increased wall bonus else fruit._stabilizeMetrics.wallContactDuration = Math.max(0, fruit._stabilizeMetrics.wallContactDuration - 1); if (fruit.surroundedFrames > 0) fruit._stabilizeMetrics.surroundedDuration += GAME_CONSTANTS.STABILIZATION_SURROUNDED_SCORE_RATE * 1.4 + fruitLevel * 0.22; // Further increased surrounded bonus 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); var fullStabilizationThreshold = GAME_CONSTANTS.STABILIZATION_THRESHOLD_FULL_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_THRESHOLD_FULL_LEVEL_FACTOR * 1.25; // Lower threshold even more var partialStabilizationThreshold = GAME_CONSTANTS.STABILIZATION_THRESHOLD_PARTIAL_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_THRESHOLD_PARTIAL_LEVEL_FACTOR * 1.25; // Lower threshold even more if (totalStabilizationScore > fullStabilizationThreshold) stabilizationLevel = 2;else if (totalStabilizationScore > partialStabilizationThreshold) stabilizationLevel = 1; if (stabilizationLevel > 0) { var baseDamp = stabilizationLevel === 2 ? GAME_CONSTANTS.STABILIZATION_DAMP_FACTOR_FULL * 0.9 : GAME_CONSTANTS.STABILIZATION_DAMP_FACTOR_PARTIAL * 0.95; // Stronger damping var dampFactor = Math.max(0.35, baseDamp - fruitLevel * GAME_CONSTANTS.STABILIZATION_DAMP_LEVEL_FACTOR * 1.1); // More aggressive damping fruit.vx *= dampFactor; fruit.vy *= dampFactor; fruit.angularVelocity *= dampFactor * 0.85; // More angular damping var stopThreshold = GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_LEVEL_FACTOR; // More assertive stopping condition - unconditionally stop when stable 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 if (movementMagnitude < stopThreshold * 0.8 && Math.abs(fruit.angularVelocity) < angularThreshold * 0.7) { 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; } } else { // Reset sleep counter if either velocity isn't near zero fruit._sleepCounter = 0; } return true; } } fruit.isFullyStabilized = false; return false; }; self.apply = function (fruit) { if (fruit.isStatic || fruit.merging) return; // Skip physics calculations for sleeping fruits, but keep collision detection 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) // More sensitive to velocity changes to un-stabilize a fruit if (fruit.isFullyStabilized && (Math.abs(fruit.vx - fruit.lastVx) > 0.7 || Math.abs(fruit.vy - fruit.lastVy) > 0.7)) { fruit.isFullyStabilized = false; } // 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 exponential scaling for gravity to make higher-level fruits dramatically heavier var gravityMultiplier = 1 + Math.pow(fruitLevel, 1.5) * 0.25; // Stronger influence 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 dramatically with fruitLevel using power function var inertiaResistance = Math.min(0.95, Math.pow(fruitLevel, 1.8) * 0.01); 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; 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 isSmallFruit = fruitLevel <= 3; var isLargeFruit = fruitLevel >= 6; var isMidAir = !fruit._boundaryContacts || !fruit._boundaryContacts.floor && !fruit._boundaryContacts.left && !fruit._boundaryContacts.right; var rotationFactor = 0.04 / (1 + fruitLevel * 0.07); var targetAngularVelocity = fruit.vx * rotationFactor; var hasNeighbors = fruit.neighborContacts && fruit.neighborContacts.length > 0; var hasManyNeighbors = hasNeighbors && fruit.neighborContacts.length >= 3; // Only apply rotation forces if moving with sufficient velocity if (movementMagnitude > 0.3) { var blendRatio = hasNeighbors ? 0.4 : 0.6; // Reduced influence when neighbors present fruit.angularVelocity = fruit.angularVelocity * (1 - blendRatio) + targetAngularVelocity * blendRatio; 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 } } // More aggressive damping for different states var sizeBasedDamp = isSmallFruit ? -0.12 : isLargeFruit ? 0.3 : 0; // Adjusted size-based damping var rotationDampFactor = 0.93; // Slightly increased base damping // Check if the fruit is rolling (in contact with surface and has meaningful linear velocity) var isRolling = !isMidAir && movementMagnitude > 0.5 && fruit._boundaryContacts && (fruit._boundaryContacts.floor || fruit._boundaryContacts.left || fruit._boundaryContacts.right); // Apply much stronger damping when not rolling if (!isRolling) { if (isMidAir) { // Extremely strong damping in mid-air rotationDampFactor = 0.3 - sizeBasedDamp; // More aggressive damping for airborne fruits } else if (movementMagnitude < 0.05) { // Almost completely stop rotation when nearly still rotationDampFactor = 0.2 - sizeBasedDamp; // Very strong damping when nearly still } else if (movementMagnitude < 0.3) { rotationDampFactor = 0.4 - sizeBasedDamp; // Strong damping at low movement } else if (hasManyNeighbors) { rotationDampFactor = 0.45 - sizeBasedDamp * 0.5; // Increased damping with many neighbors } else if (hasNeighbors) { rotationDampFactor = 0.5 - sizeBasedDamp * 0.75; // Increased damping with neighbors } else { rotationDampFactor = 0.6 - sizeBasedDamp * 0.7; // Default non-rolling damping } } else { // Less aggressive damping when rolling if (movementMagnitude < 0.5) { rotationDampFactor = 0.75 - sizeBasedDamp * 0.7; // Moderate damping at medium movement } else if (movementMagnitude < 0.8) { rotationDampFactor = 0.82 - sizeBasedDamp * 0.5; // Less damping at higher movement } } // 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; } // Make angular damping heavily dependent on linear velocity if (movementMagnitude < GAME_CONSTANTS.STABILIZATION_MOVEMENT_THRESHOLD_BASE) { // Apply extreme angular damping when linear movement is minimal var linearDampingFactor = movementMagnitude < 0.1 ? 0.1 : 0.3; fruit.angularVelocity *= linearDampingFactor; // Directly zero out tiny angular velocities when there's almost no movement if (Math.abs(fruit.angularVelocity) < 0.01 || movementMagnitude < 0.05) { 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) { var rollFactor = 0.025 - fruitLevel * 0.001; var idealRollingVelocity = fruit.vx * rollFactor; fruit.angularVelocity = fruit.angularVelocity * 0.7 + idealRollingVelocity * 0.3; // Increased influence of ideal rolling velocity } 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.7; // Stronger wall damping } // Much more aggressive resting threshold for rotation var angularThreshold = isSmallFruit ? 0.004 : 0.002; // Further reduced threshold for stopping rotation var restFramesThreshold = isSmallFruit ? 2 : 2; // Even fewer frames required to stabilize if (Math.abs(fruit.angularVelocity) < angularThreshold) { fruit.rotationRestCounter++; if (fruit.rotationRestCounter > restFramesThreshold) { fruit.angularVelocity = 0; } } else { fruit.rotationRestCounter = 0; } var maxAngularMultiplier = isSmallFruit ? 1.2 : 1.4; 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.90, // Increased base friction ANGULAR_FRICTION: 0.80, 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 }, _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.1), "ROTATION_TRANSFER_FACTOR", 0.01), "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 = 1; gameOverLine.scaleY = 0.2; gameOverLine.alpha = 1; } function createNextFruit() { var fruitProbability = Math.random(); var fruitType = fruitProbability < 0.6 ? FruitTypes.CHERRY : FruitTypes.GRAPE; nextFruitType = fruitType; activeFruit = new Fruit(nextFruitType); activeFruit.x = lastDroppedFruit && lastDroppedFruit.x ? lastDroppedFruit.x : gameWidth / 2; activeFruit.y = dropPointY + GAME_CONSTANTS.DROP_START_Y_OFFSET; activeFruit.isStatic = true; activeFruit.hasBounced = false; 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); orange.hasBounced = false; 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.hasBounced = false; // Reset bounce state on drop fruit.isFullyStabilized = false; // Reset stabilization state fruit.isSleeping = false; // Wake up the fruit when dropping if (fruit._sleepCounter) fruit._sleepCounter = 0; // Reset sleep counter 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); var mass1 = Math.pow(level1, 1.5); var mass2 = Math.pow(level2, 1.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; var mass1 = Math.pow(level1, 2.0) * Math.pow(fruit1.type.size, 1.0); // Simplified mass calc var mass2 = Math.pow(level2, 2.0) * Math.pow(fruit2.type.size, 1.0); var totalMass = mass1 + mass2; 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; // Adjusted damping var impulse1 = impulse * impulseRatio1 * velocityDampening; var impulse2 = impulse * impulseRatio2 * velocityDampening; // Apply impulses with scaled values for stabilized fruits var impactThreshold = 1.2; // Threshold to determine significant impacts // For first fruit: apply reduced impulse if stabilized, normal otherwise // Wake up sleeping fruits on significant impacts 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 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 reduced impulse if stabilized, normal otherwise 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 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; var frictionImpulse = -tangentVelocity * 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; } 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.2; // Increased threshold for generating rotational force // Calculate angular impulse with adjustment for impact magnitude var angularImpulse = 0; if (tangentialMagnitude > angularThreshold || contactMagnitude > angularThreshold) { // Apply a curve that increases rotation for more significant impacts var impactFactor = Math.min(1.0, (Math.max(tangentialMagnitude, contactMagnitude) - angularThreshold) / 2.0); angularImpulse = tangentialComponent * GAME_CONSTANTS.ROTATION_TRANSFER_FACTOR * impactFactor; } // 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.95; } if (!fruit2.isFullyStabilized && angularImpulse !== 0) { fruit2.angularVelocity -= inertia1 > 0 ? angularImpulse * (inertia2 / (inertia1 + inertia2)) : angularImpulse * 0.5; fruit2.angularVelocity *= 0.95; } // 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; if (fruits[j]._sleepCounter) fruits[j]._sleepCounter = 0; // 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
@@ -720,23 +720,31 @@
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;
- var gravityMultiplier = 1 + fruitLevel * GAME_CONSTANTS.GRAVITY_LEVEL_MULTIPLIER;
+ // Use exponential scaling for gravity to make higher-level fruits dramatically heavier
+ var gravityMultiplier = 1 + Math.pow(fruitLevel, 1.5) * 0.25; // Stronger influence
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));
- fruit.vy += fruitLevel * GAME_CONSTANTS.LEVEL_DOWNWARD_FORCE_FACTOR;
+ // 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;
- var inertiaResistance = Math.min(0.9, fruitLevel * GAME_CONSTANTS.LEVEL_INERTIA_FACTOR);
+ // Make inertia scale dramatically with fruitLevel using power function
+ var inertiaResistance = Math.min(0.95, Math.pow(fruitLevel, 1.8) * 0.01);
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
@@ -749,11 +757,12 @@
// 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;
- var frictionModifier = 0.99 + fruitLevel * 0.003;
+ // 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.97 : 0.99;
+ 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;
var stopThreshold = GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_LEVEL_FACTOR;
stopThreshold = Math.max(0.05, stopThreshold);
@@ -854,11 +863,11 @@
// Increase wall damping
if (fruit._boundaryContacts && (fruit._boundaryContacts.left || fruit._boundaryContacts.right) && Math.abs(fruit.angularVelocity) > 0.01) {
fruit.angularVelocity *= 0.7; // Stronger wall damping
}
- // More aggressive resting threshold for rotation
- var angularThreshold = isSmallFruit ? 0.005 : 0.003; // Reduced threshold for stopping rotation
- var restFramesThreshold = isSmallFruit ? 2 : 3; // Fewer frames required to stabilize
+ // Much more aggressive resting threshold for rotation
+ var angularThreshold = isSmallFruit ? 0.004 : 0.002; // Further reduced threshold for stopping rotation
+ var restFramesThreshold = isSmallFruit ? 2 : 2; // Even fewer frames required to stabilize
if (Math.abs(fruit.angularVelocity) < angularThreshold) {
fruit.rotationRestCounter++;
if (fruit.rotationRestCounter > restFramesThreshold) {
fruit.angularVelocity = 0;
@@ -1110,12 +1119,13 @@
// Physics
BASE_GRAVITY: 5.0,
GRAVITY_LEVEL_MULTIPLIER: 0.4,
// Adjusted gravity scaling
- FRICTION: 0.88,
+ FRICTION: 0.90,
+ // Increased base friction
ANGULAR_FRICTION: 0.80,
- GROUND_ANGULAR_FRICTION: 0.50,
- // Increased ground friction
+ 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,