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.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; var angularThreshold = GAME_CONSTANTS.STABILIZATION_ANGULAR_THRESHOLD_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_ANGULAR_THRESHOLD_LEVEL_FACTOR; if (movementMagnitude < movementThreshold && Math.abs(fruit.angularVelocity) < angularThreshold) { var stabilizationRate = GAME_CONSTANTS.STABILIZATION_SCORE_RATE_BASE + fruitLevel * GAME_CONSTANTS.STABILIZATION_SCORE_LEVEL_FACTOR; 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 + fruitLevel * 0.1;else fruit._stabilizeMetrics.wallContactDuration = Math.max(0, fruit._stabilizeMetrics.wallContactDuration - 1); if (fruit.surroundedFrames > 0) fruit._stabilizeMetrics.surroundedDuration += GAME_CONSTANTS.STABILIZATION_SURROUNDED_SCORE_RATE + fruitLevel * 0.15;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; var partialStabilizationThreshold = GAME_CONSTANTS.STABILIZATION_THRESHOLD_PARTIAL_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_THRESHOLD_PARTIAL_LEVEL_FACTOR; if (totalStabilizationScore > fullStabilizationThreshold) stabilizationLevel = 2;else if (totalStabilizationScore > partialStabilizationThreshold) stabilizationLevel = 1; if (stabilizationLevel > 0) { var baseDamp = stabilizationLevel === 2 ? GAME_CONSTANTS.STABILIZATION_DAMP_FACTOR_FULL : GAME_CONSTANTS.STABILIZATION_DAMP_FACTOR_PARTIAL; var dampFactor = Math.max(0.4, baseDamp - fruitLevel * GAME_CONSTANTS.STABILIZATION_DAMP_LEVEL_FACTOR); fruit.vx *= dampFactor; fruit.vy *= dampFactor; fruit.angularVelocity *= dampFactor * 0.9; var stopThreshold = GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_LEVEL_FACTOR; if (stabilizationLevel === 2 && movementMagnitude < stopThreshold) { fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; fruit.isFullyStabilized = true; return true; } } fruit.isFullyStabilized = false; return false; }; self.apply = function (fruit) { if (fruit.isStatic || fruit.merging) return; // Reset stabilization flag if significant external force applied (e.g., explosion) if (fruit.isFullyStabilized && (Math.abs(fruit.vx - fruit.lastVx) > 1.0 || Math.abs(fruit.vy - fruit.lastVy) > 1.0)) { fruit.isFullyStabilized = false; } // Skip physics application if fully stabilized AND not being pushed significantly if (fruit.isFullyStabilized && Math.abs(fruit.vx) < 0.5 && Math.abs(fruit.vy) < 0.5) { fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; // Ensure it stays stopped 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; var gravityMultiplier = 1 + fruitLevel * GAME_CONSTANTS.GRAVITY_LEVEL_MULTIPLIER; 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; 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); 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); var isFullyStabilizedByFunc = self.stabilizeFruit(fruit, movementMagnitude, velocityChange); fruit.isFullyStabilized = isFullyStabilizedByFunc; // Update fruit's state if (!fruit.isFullyStabilized) { if (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 { fruit.angularVelocity *= 0.8; } fruit.rotation += fruit.angularVelocity; var frictionModifier = 0.99 + fruitLevel * 0.003; fruit.vx *= fruit.friction * frictionModifier; fruit.vy *= fruit.friction * frictionModifier * 0.99; 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 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; if (movementMagnitude > 0.3) { var blendRatio = hasNeighbors ? 0.5 : 0.7; 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.3; } } var sizeBasedDamp = isSmallFruit ? -0.15 : isLargeFruit ? 0.25 : 0; var rotationDampFactor = 0.95; if (hasManyNeighbors) rotationDampFactor = 0.5 - sizeBasedDamp * 0.5;else if (hasNeighbors) rotationDampFactor = 0.65 - sizeBasedDamp * 0.75;else if (movementMagnitude < 0.05) rotationDampFactor = 0.6 - sizeBasedDamp;else if (movementMagnitude < 0.3) rotationDampFactor = 0.7 - sizeBasedDamp;else if (movementMagnitude < 0.5) rotationDampFactor = 0.8 - sizeBasedDamp * 0.7;else if (movementMagnitude < 0.8) rotationDampFactor = 0.85 - sizeBasedDamp * 0.5; fruit.angularVelocity *= rotationDampFactor; if (fruit.lastVx !== undefined && Math.abs(fruit.vx - fruit.lastVx) > 0.5) { fruit.angularVelocity -= (fruit.vx - fruit.lastVx) * 0.02; } if (fruit._boundaryContacts && fruit._boundaryContacts.floor && Math.abs(fruit.vx) > 0.2) { var rollFactor = 0.025 - fruitLevel * 0.001; var idealRollingVelocity = fruit.vx * rollFactor; fruit.angularVelocity = fruit.angularVelocity * 0.75 + idealRollingVelocity * 0.25; } if (fruit._boundaryContacts && (fruit._boundaryContacts.left || fruit._boundaryContacts.right) && Math.abs(fruit.angularVelocity) > 0.01) { fruit.angularVelocity *= 0.8; } var angularThreshold = isSmallFruit ? 0.01 : 0.006; var restFramesThreshold = isSmallFruit ? 3 : 5; 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 = { 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.92, ANGULAR_FRICTION: 0.85, GROUND_ANGULAR_FRICTION: 0.5, // Increased ground friction MAX_ANGULAR_VELOCITY: 0.15, ELASTICITY_HIGH: 0.85, ELASTICITY_LOW_START_LEVEL: 4, ELASTICITY_LOW_BASE: 0.8, ELASTICITY_DECREASE_FACTOR: 0.2 / 9, // (0.8 - 0.6) / (10-1) approx 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, BOUNCE_SOUND_VELOCITY_THRESHOLD: 0.5, COLLISION_SEPARATION_FACTOR: 1.05, 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, 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, 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, // Charged Ball / Portal CHARGE_NEEDED_FOR_RELEASE: 15, PORTAL_UI_Y: 120, PORTAL_UI_X_OFFSET: 870, PORTAL_TWEEN_DURATION: 300, PORTAL_PULSE_DURATION: 500, // Pineapple PINEAPPLE_MERGES_NEEDED: 15, PINEAPPLE_START_Y: 200, PINEAPPLE_END_POS_FACTOR: 0.16, PINEAPPLE_TWEEN_DURATION: 300, PINEAPPLE_IMMUNITY_MS: 3000, // Fire Background FIRE_BASE_COUNT: 3, FIRE_FRUIT_TYPE_THRESHOLD: 1, // 1 new fire per unique type FIRE_MAX_COUNT: 15, FIRE_START_Y_OFFSET: 50, FIRE_STACK_Y_OFFSET: 100, FIRE_X_SPREAD: 500, 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 COCONUT_SPAWN_SCORE_INTERVAL: 1000, // Evolution Line UI EVOLUTION_LINE_Y: 120, EVOLUTION_LINE_X_OFFSET: 350, EVOLUTION_ICON_MAX_SIZE: 150, EVOLUTION_ICON_SPACING: 20, // Score UI 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.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 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; } var overlap = Math.min(combinedHalfWidths - absDistanceX, combinedHalfHeights - absDistanceY); if (overlap < 0) overlap = 0; // Ensure overlap is non-negative var normalizeX = dx / distance; var normalizeY = dy / distance; var moveX = overlap / 2 * normalizeX; var moveY = overlap / 2 * normalizeY; fruit1.x -= moveX * GAME_CONSTANTS.COLLISION_SEPARATION_FACTOR; fruit1.y -= moveY * GAME_CONSTANTS.COLLISION_SEPARATION_FACTOR; fruit2.x += moveX * GAME_CONSTANTS.COLLISION_SEPARATION_FACTOR; fruit2.y += moveY * GAME_CONSTANTS.COLLISION_SEPARATION_FACTOR; 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 only if fruits are not fully stabilized if (!fruit1.isFullyStabilized) { fruit1.vx -= impulse1 * normalizeX; fruit1.vy -= impulse1 * normalizeY; } if (!fruit2.isFullyStabilized) { fruit2.vx += impulse2 * normalizeX; fruit2.vy += impulse2 * normalizeY; } // If impact is large enough, un-stabilize if (Math.abs(impulse1) > 1.5 && fruit1.isFullyStabilized) fruit1.isFullyStabilized = false; if (Math.abs(impulse2) > 1.5 && fruit2.isFullyStabilized) fruit2.isFullyStabilized = false; var tangentX = -normalizeY; var tangentY = normalizeX; var tangentVelocity = rvX * tangentX + rvY * tangentY; var frictionImpulse = -tangentVelocity * GAME_CONSTANTS.INTER_FRUIT_FRICTION; 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); var angularImpulse = tangentialComponent * GAME_CONSTANTS.ROTATION_TRANSFER_FACTOR; if (!fruit1.isFullyStabilized) { fruit1.angularVelocity += inertia2 > 0 ? angularImpulse * (inertia1 / (inertia1 + inertia2)) : angularImpulse * 0.5; fruit1.angularVelocity *= 0.95; } if (!fruit2.isFullyStabilized) { fruit2.angularVelocity -= inertia1 > 0 ? angularImpulse * (inertia2 / (inertia1 + inertia2)) : angularImpulse * 0.5; fruit2.angularVelocity *= 0.95; } if (Math.abs(angularImpulse) > 0.01) { // 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); } for (var i = fruits.length - 1; i >= 0; i--) { var fruit = fruits[i]; if (!fruit || fruit.isStatic || fruit.merging) continue; fruit.updatePhysics(); var walls = { left: wallLeft, right: wallRight }; fruit.checkBoundaries(walls, gameFloor); } checkFruitCollisions(); for (var i = 0; i < fruits.length; i++) { var fruit = fruits[i]; if (fruit && !fruit.isStatic && !fruit.merging) { if (fruit.surroundedFrames === undefined) fruit.surroundedFrames = 0; var boundaryContact = fruit.wallContactFrames > 0; var floorProximity = fruit.y + fruit.height / 2 >= gameFloor.y - gameFloor.height / 2 - 10; var hasMultipleNeighbors = fruit.neighborContacts && fruit.neighborContacts.length >= 2; var isSlowMoving = Math.abs(fruit.vx) < 0.3 && Math.abs(fruit.vy) < 0.3; var hasGapBelow = false; if (floorProximity || hasMultipleNeighbors) { var potentialNeighbors = spatialGrid.getPotentialCollisions({ x: fruit.x, y: fruit.y + fruit.height / 2 + 20, width: fruit.width * 0.6, height: 20, id: 'gap_check_' + fruit.id }); hasGapBelow = potentialNeighbors.length === 0 && !floorProximity; if (hasGapBelow && isSlowMoving) { var leftCount = 0, rightCount = 0; for (var j = 0; j < fruit.neighborContacts.length; j++) { var neighborId = fruit.neighborContacts[j]; for (var k = 0; k < fruits.length; k++) { if (fruits[k] && fruits[k].id === neighborId) { if (fruits[k].x < fruit.x) leftCount++;else if (fruits[k].x > fruit.x) rightCount++; break; } } } if (leftCount < rightCount) fruit.vx -= 0.05;else if (rightCount < leftCount) fruit.vx += 0.05;else fruit.vx += Math.random() * 0.1 - 0.05; } } // Removed stabilizationScenario switch; handled within PhysicsComponent now 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) { var index = fruits.indexOf(fruit); if (index !== -1) fruits.splice(index, 1); spatialGrid.removeObject(fruit); fruit.destroy(); }
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var ChargedBallUI = Container.expand(function () {
var self = Container.call(this);
self.chargeNeededForRelease = 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.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;
var angularThreshold = GAME_CONSTANTS.STABILIZATION_ANGULAR_THRESHOLD_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_ANGULAR_THRESHOLD_LEVEL_FACTOR;
if (movementMagnitude < movementThreshold && Math.abs(fruit.angularVelocity) < angularThreshold) {
var stabilizationRate = GAME_CONSTANTS.STABILIZATION_SCORE_RATE_BASE + fruitLevel * GAME_CONSTANTS.STABILIZATION_SCORE_LEVEL_FACTOR;
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 + fruitLevel * 0.1;else fruit._stabilizeMetrics.wallContactDuration = Math.max(0, fruit._stabilizeMetrics.wallContactDuration - 1);
if (fruit.surroundedFrames > 0) fruit._stabilizeMetrics.surroundedDuration += GAME_CONSTANTS.STABILIZATION_SURROUNDED_SCORE_RATE + fruitLevel * 0.15;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;
var partialStabilizationThreshold = GAME_CONSTANTS.STABILIZATION_THRESHOLD_PARTIAL_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_THRESHOLD_PARTIAL_LEVEL_FACTOR;
if (totalStabilizationScore > fullStabilizationThreshold) stabilizationLevel = 2;else if (totalStabilizationScore > partialStabilizationThreshold) stabilizationLevel = 1;
if (stabilizationLevel > 0) {
var baseDamp = stabilizationLevel === 2 ? GAME_CONSTANTS.STABILIZATION_DAMP_FACTOR_FULL : GAME_CONSTANTS.STABILIZATION_DAMP_FACTOR_PARTIAL;
var dampFactor = Math.max(0.4, baseDamp - fruitLevel * GAME_CONSTANTS.STABILIZATION_DAMP_LEVEL_FACTOR);
fruit.vx *= dampFactor;
fruit.vy *= dampFactor;
fruit.angularVelocity *= dampFactor * 0.9;
var stopThreshold = GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_BASE - fruitLevel * GAME_CONSTANTS.STABILIZATION_STOP_THRESHOLD_LEVEL_FACTOR;
if (stabilizationLevel === 2 && movementMagnitude < stopThreshold) {
fruit.vx = 0;
fruit.vy = 0;
fruit.angularVelocity = 0;
fruit.isFullyStabilized = true;
return true;
}
}
fruit.isFullyStabilized = false;
return false;
};
self.apply = function (fruit) {
if (fruit.isStatic || fruit.merging) return;
// Reset stabilization flag if significant external force applied (e.g., explosion)
if (fruit.isFullyStabilized && (Math.abs(fruit.vx - fruit.lastVx) > 1.0 || Math.abs(fruit.vy - fruit.lastVy) > 1.0)) {
fruit.isFullyStabilized = false;
}
// Skip physics application if fully stabilized AND not being pushed significantly
if (fruit.isFullyStabilized && Math.abs(fruit.vx) < 0.5 && Math.abs(fruit.vy) < 0.5) {
fruit.vx = 0;
fruit.vy = 0;
fruit.angularVelocity = 0; // Ensure it stays stopped
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;
var gravityMultiplier = 1 + fruitLevel * GAME_CONSTANTS.GRAVITY_LEVEL_MULTIPLIER;
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;
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);
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);
var isFullyStabilizedByFunc = self.stabilizeFruit(fruit, movementMagnitude, velocityChange);
fruit.isFullyStabilized = isFullyStabilizedByFunc; // Update fruit's state
if (!fruit.isFullyStabilized) {
if (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 {
fruit.angularVelocity *= 0.8;
}
fruit.rotation += fruit.angularVelocity;
var frictionModifier = 0.99 + fruitLevel * 0.003;
fruit.vx *= fruit.friction * frictionModifier;
fruit.vy *= fruit.friction * frictionModifier * 0.99;
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 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;
if (movementMagnitude > 0.3) {
var blendRatio = hasNeighbors ? 0.5 : 0.7;
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.3;
}
}
var sizeBasedDamp = isSmallFruit ? -0.15 : isLargeFruit ? 0.25 : 0;
var rotationDampFactor = 0.95;
if (hasManyNeighbors) rotationDampFactor = 0.5 - sizeBasedDamp * 0.5;else if (hasNeighbors) rotationDampFactor = 0.65 - sizeBasedDamp * 0.75;else if (movementMagnitude < 0.05) rotationDampFactor = 0.6 - sizeBasedDamp;else if (movementMagnitude < 0.3) rotationDampFactor = 0.7 - sizeBasedDamp;else if (movementMagnitude < 0.5) rotationDampFactor = 0.8 - sizeBasedDamp * 0.7;else if (movementMagnitude < 0.8) rotationDampFactor = 0.85 - sizeBasedDamp * 0.5;
fruit.angularVelocity *= rotationDampFactor;
if (fruit.lastVx !== undefined && Math.abs(fruit.vx - fruit.lastVx) > 0.5) {
fruit.angularVelocity -= (fruit.vx - fruit.lastVx) * 0.02;
}
if (fruit._boundaryContacts && fruit._boundaryContacts.floor && Math.abs(fruit.vx) > 0.2) {
var rollFactor = 0.025 - fruitLevel * 0.001;
var idealRollingVelocity = fruit.vx * rollFactor;
fruit.angularVelocity = fruit.angularVelocity * 0.75 + idealRollingVelocity * 0.25;
}
if (fruit._boundaryContacts && (fruit._boundaryContacts.left || fruit._boundaryContacts.right) && Math.abs(fruit.angularVelocity) > 0.01) {
fruit.angularVelocity *= 0.8;
}
var angularThreshold = isSmallFruit ? 0.01 : 0.006;
var restFramesThreshold = isSmallFruit ? 3 : 5;
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 = {
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.92,
ANGULAR_FRICTION: 0.85,
GROUND_ANGULAR_FRICTION: 0.5,
// Increased ground friction
MAX_ANGULAR_VELOCITY: 0.15,
ELASTICITY_HIGH: 0.85,
ELASTICITY_LOW_START_LEVEL: 4,
ELASTICITY_LOW_BASE: 0.8,
ELASTICITY_DECREASE_FACTOR: 0.2 / 9,
// (0.8 - 0.6) / (10-1) approx
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,
BOUNCE_SOUND_VELOCITY_THRESHOLD: 0.5,
COLLISION_SEPARATION_FACTOR: 1.05,
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,
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,
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,
// Charged Ball / Portal
CHARGE_NEEDED_FOR_RELEASE: 15,
PORTAL_UI_Y: 120,
PORTAL_UI_X_OFFSET: 870,
PORTAL_TWEEN_DURATION: 300,
PORTAL_PULSE_DURATION: 500,
// Pineapple
PINEAPPLE_MERGES_NEEDED: 15,
PINEAPPLE_START_Y: 200,
PINEAPPLE_END_POS_FACTOR: 0.16,
PINEAPPLE_TWEEN_DURATION: 300,
PINEAPPLE_IMMUNITY_MS: 3000,
// Fire Background
FIRE_BASE_COUNT: 3,
FIRE_FRUIT_TYPE_THRESHOLD: 1,
// 1 new fire per unique type
FIRE_MAX_COUNT: 15,
FIRE_START_Y_OFFSET: 50,
FIRE_STACK_Y_OFFSET: 100,
FIRE_X_SPREAD: 500,
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
COCONUT_SPAWN_SCORE_INTERVAL: 1000,
// Evolution Line UI
EVOLUTION_LINE_Y: 120,
EVOLUTION_LINE_X_OFFSET: 350,
EVOLUTION_ICON_MAX_SIZE: 150,
EVOLUTION_ICON_SPACING: 20,
// Score UI
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.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
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;
}
var overlap = Math.min(combinedHalfWidths - absDistanceX, combinedHalfHeights - absDistanceY);
if (overlap < 0) overlap = 0; // Ensure overlap is non-negative
var normalizeX = dx / distance;
var normalizeY = dy / distance;
var moveX = overlap / 2 * normalizeX;
var moveY = overlap / 2 * normalizeY;
fruit1.x -= moveX * GAME_CONSTANTS.COLLISION_SEPARATION_FACTOR;
fruit1.y -= moveY * GAME_CONSTANTS.COLLISION_SEPARATION_FACTOR;
fruit2.x += moveX * GAME_CONSTANTS.COLLISION_SEPARATION_FACTOR;
fruit2.y += moveY * GAME_CONSTANTS.COLLISION_SEPARATION_FACTOR;
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 only if fruits are not fully stabilized
if (!fruit1.isFullyStabilized) {
fruit1.vx -= impulse1 * normalizeX;
fruit1.vy -= impulse1 * normalizeY;
}
if (!fruit2.isFullyStabilized) {
fruit2.vx += impulse2 * normalizeX;
fruit2.vy += impulse2 * normalizeY;
}
// If impact is large enough, un-stabilize
if (Math.abs(impulse1) > 1.5 && fruit1.isFullyStabilized) fruit1.isFullyStabilized = false;
if (Math.abs(impulse2) > 1.5 && fruit2.isFullyStabilized) fruit2.isFullyStabilized = false;
var tangentX = -normalizeY;
var tangentY = normalizeX;
var tangentVelocity = rvX * tangentX + rvY * tangentY;
var frictionImpulse = -tangentVelocity * GAME_CONSTANTS.INTER_FRUIT_FRICTION;
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);
var angularImpulse = tangentialComponent * GAME_CONSTANTS.ROTATION_TRANSFER_FACTOR;
if (!fruit1.isFullyStabilized) {
fruit1.angularVelocity += inertia2 > 0 ? angularImpulse * (inertia1 / (inertia1 + inertia2)) : angularImpulse * 0.5;
fruit1.angularVelocity *= 0.95;
}
if (!fruit2.isFullyStabilized) {
fruit2.angularVelocity -= inertia1 > 0 ? angularImpulse * (inertia2 / (inertia1 + inertia2)) : angularImpulse * 0.5;
fruit2.angularVelocity *= 0.95;
}
if (Math.abs(angularImpulse) > 0.01) {
// 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);
}
for (var i = fruits.length - 1; i >= 0; i--) {
var fruit = fruits[i];
if (!fruit || fruit.isStatic || fruit.merging) continue;
fruit.updatePhysics();
var walls = {
left: wallLeft,
right: wallRight
};
fruit.checkBoundaries(walls, gameFloor);
}
checkFruitCollisions();
for (var i = 0; i < fruits.length; i++) {
var fruit = fruits[i];
if (fruit && !fruit.isStatic && !fruit.merging) {
if (fruit.surroundedFrames === undefined) fruit.surroundedFrames = 0;
var boundaryContact = fruit.wallContactFrames > 0;
var floorProximity = fruit.y + fruit.height / 2 >= gameFloor.y - gameFloor.height / 2 - 10;
var hasMultipleNeighbors = fruit.neighborContacts && fruit.neighborContacts.length >= 2;
var isSlowMoving = Math.abs(fruit.vx) < 0.3 && Math.abs(fruit.vy) < 0.3;
var hasGapBelow = false;
if (floorProximity || hasMultipleNeighbors) {
var potentialNeighbors = spatialGrid.getPotentialCollisions({
x: fruit.x,
y: fruit.y + fruit.height / 2 + 20,
width: fruit.width * 0.6,
height: 20,
id: 'gap_check_' + fruit.id
});
hasGapBelow = potentialNeighbors.length === 0 && !floorProximity;
if (hasGapBelow && isSlowMoving) {
var leftCount = 0,
rightCount = 0;
for (var j = 0; j < fruit.neighborContacts.length; j++) {
var neighborId = fruit.neighborContacts[j];
for (var k = 0; k < fruits.length; k++) {
if (fruits[k] && fruits[k].id === neighborId) {
if (fruits[k].x < fruit.x) leftCount++;else if (fruits[k].x > fruit.x) rightCount++;
break;
}
}
}
if (leftCount < rightCount) fruit.vx -= 0.05;else if (rightCount < leftCount) fruit.vx += 0.05;else fruit.vx += Math.random() * 0.1 - 0.05;
}
}
// Removed stabilizationScenario switch; handled within PhysicsComponent now
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) {
var index = fruits.indexOf(fruit);
if (index !== -1) fruits.splice(index, 1);
spatialGrid.removeObject(fruit);
fruit.destroy();
}