User prompt
Refactor the `releasePineappleOnMerge` function. Capture the `pineapple` instance that is about to be released into a local variable *before* modifying it. Make this captured instance dynamic and add it to the fruits array and spatial grid. Immediately after this, create a *new* static pineapple instance, set its initial properties (position, isStatic), add it to the game, and assign it to the global `pineapple` variable. Modify the `setTimeout` callback to only handle the end of immunity for the *captured released instance* and remove the call to `setupPineapple()` and the resetting of `pineappleActive`.
User prompt
The `completeMerge` function has conditional logic for removing `fruit2` based on whether `fruit1` was a Durian. Since the Durian's `onMerge` behavior explicitly handles the removal of both fruits when they merge, the conditional removal in the central `completeMerge` becomes redundant and potentially confusing. Suggested Solution: Simplify the fruit removal logic in `completeMerge`. Unconditionally remove both `fruit1` and `fruit2` after the fruit behavior's `onMerge` method has been called, as the Durian's behavior will have already handled its specific case. Instructions for AI Game Engine: In the `completeMerge` function, remove the `if (fruit2.type.id.toUpperCase() !== 'DURIAN' || fruit1.type.id.toUpperCase() !== 'DURIAN')` condition surrounding the `removeFruitFromGame(fruit2)` call. Ensure that `removeFruitFromGame(fruit1)` and `removeFruitFromGame(fruit2)` are called unconditionally after the lines that increment `mergeCounter` and check/release the pineapple.
User prompt
Please fix the bug: 'Uncaught TypeError: self.ellipseEllipseIntersect is not a function' in or related to this line: 'if (self.ellipseEllipseIntersect(startX, thisY, activeFruit.width, activeFruit.height, fruit.x, fruit.y, fruit.width, fruit.height)) {' Line Number: 1167
User prompt
Identified Problem: The `ellipseEllipseIntersect` function is defined both as a global helper function and as a method within the `TrajectoryLine` class. This creates unnecessary code duplication. Suggested Solution: Remove the redundant method definition from the `TrajectoryLine` class and ensure the class's logic correctly calls the globally defined `ellipseEllipseIntersect` function. Instructions for AI Game Engine: Delete the method `self.ellipseEllipseIntersect = function(...)` from the `TrajectoryLine` class. Verify that the `updateTrajectory` method continues to correctly call the globally defined `ellipseEllipseIntersect` function.
User prompt
The `pushPineapple` function is called after *every* merge (`completeMerge`), even when the special pineapple hasn't been activated yet (`!pineappleActive`). While it contains a check `if (!pineappleActive && pineapple)`, calling it unnecessarily adds overhead. Suggested Solution: Only call the `pushPineapple` function when it's actually relevant, likely tied to the `mergeCounter` reaching certain thresholds or specifically when the pineapple is in its static, waiting state and a merge occurs. Instructions for AI Game Engine: Review the `completeMerge` function. Remove the direct call to `pushPineapple()` from this function. Modify the logic where `mergeCounter` is incremented to check if the increment results in a threshold being met (e.g., `mergeCounter <= GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED`). If so, call `pushPineapple()` at that point, ensuring the call is conditional on `!pineappleActive` and `pineapple` existing.
User prompt
The logic for spawning the special charged Orange (Pickle Rick) is tightly coupled with the general `dropFruit` function, making the `dropFruit` function responsible for both standard fruit dropping and special spawn effects. Suggested Solution: Decouple the special orange spawn logic from the main `dropFruit` function. Create a separate function specifically for handling the charged ball release effect, which is called when the conditions are met (e.g., triggered by an event listener or check in `game.update`). Instructions for AI Game Engine: Extract the code block related to checking `readyToReleaseCharged` and spawning the orange from `dropFruit` into a new function, e.g., `handleChargedRelease`. Modify `dropFruit` to simply increment the charge counter and update the UI. Add a check in the main `game.update` loop or an event handler to call `handleChargedRelease` when `readyToReleaseCharged` is true and `chargeCounter` is sufficient.
User prompt
Several physics and collision constants or multipliers are hardcoded as magic numbers within the `PhysicsComponent` and `CollisionComponent` methods instead of being defined and referenced from the `GAME_CONSTANTS` object. Suggested Solution: Move all significant numeric values used for physics and collision calculations into the `GAME_CONSTANTS` object with descriptive names. Reference these constants within the component methods to improve readability, maintainability, and ease of tuning. Instructions for AI Game Engine: Identify hardcoded numeric values within `PhysicsComponent.apply`, `PhysicsComponent.handleRotationLimits`, `PhysicsComponent.checkStabilization`, `CollisionComponent` methods, and `checkFruitCollisions`. Add these values as new properties to the `GAME_CONSTANTS` object with clear names (e.g., `COLLISION_IMPULSE_REDUCTION`, `ROTATIONAL_FRICTION_STRENGTH`). Replace the magic numbers in the code with references to these new constants.
User prompt
Please fix the bug: 'TypeError: TrajectoryLine.prototype.ellipseEllipseIntersect is not a function' in or related to this line: 'shouldMerge = TrajectoryLine.prototype.ellipseEllipseIntersect(fruit1.x, fruit1.y, fruit1.width, fruit1.height, fruit2.x, fruit2.y, fruit2.width, fruit2.height);' Line Number: 1874
User prompt
Identified Problem: The `ellipseEllipseIntersect` function logic is duplicated in both the `TrajectoryLine` class and the `checkFruitCollisions` global function. Suggested Solution: Eliminate code duplication by defining the `ellipseEllipseIntersect` logic in a single, accessible location. This could be a static method on a utility class, a standalone global helper function, or part of a dedicated collision helper. Instructions for AI Game Engine: Remove the duplicate `ellipseEllipseIntersect` function definition from `checkFruitCollisions`. Ensure that `checkFruitCollisions` correctly references and uses the `ellipseEllipseIntersect` method defined within the `TrajectoryLine` class or another single source
User prompt
Please fix the bug: 'Error: Invalid end type for property rotation: undefined' in or related to this line: 'tween(chargedBallUI.portalAsset, {' Line Number: 2425
User prompt
Identify where `tween()` is called inside `ChargedBallUI` and `FireElement` methods. Create corresponding event emitters or state change checks in the global game loop (`game.update`) or relevant input handlers (`game.up`, `game.down`). Modify the class methods to signal these state changes. Move the `tween()` calls from the class methods into the global game logic that responds to these signals.
User prompt
To help with the LK.setTimeout in releasePineappleOnMerge(): You need to ensure the timeout acts on the correct instance if the global pineapple is replaced by setupPineapple() before the timeout fires. // Inside releasePineappleOnMerge's if block: if (mergeCounter >= GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED && !pineappleActive && pineapple) { pineappleActive = true; // ... (visibility fixes for the 'pineapple' instance about to be released) ... var currentlyReleasedPineapple = pineapple; // Capture THIS instance currentlyReleasedPineapple.isStatic = false; applyDropPhysics(currentlyReleasedPineapple, 1.5); fruits.push(currentlyReleasedPineapple); if (spatialGrid) { spatialGrid.insertObject(currentlyReleasedPineapple); } mergeCounter = 0; // Reset for the next cycle *now* LK.setTimeout(function () { if (currentlyReleasedPineapple && fruits.indexOf(currentlyReleasedPineapple) !== -1) { currentlyReleasedPineapple.immuneToGameOver = false; // ... (visibility re-assertions for 'currentlyReleasedPineapple') ... } // This part prepares for the *next* pineapple cycle pineappleActive = false; setupPineapple(); // This creates a new global 'pineapple' }, GAME_CONSTANTS.PINEAPPLE_IMMUNITY_MS); } Use code with caution. JavaScript Ava, the invisibility strongly suggests an unintended modification to alpha, visible, or scale. Stopping all tweens on the instance before making it dynamic, and explicitly setting these properties should override any stray effects. The logical flow for mergeCounter and pineappleActive needs to be extremely precise to ensure the cycle works as intended. Good luck!
User prompt
Objective 3: Clean setupPineapple() for a Fresh Start. Action for Ava: In setupPineapple(): When the new pineapple = new Fruit(FruitTypes.PINEAPPLE); is created: pineapple.isStatic = true; (Aggressive visibility fix: pineapple.alpha = 1.0; pineapple.visible = true; pineapple.scale.set(1.0); and for its children). pineappleActive = false; (This new static pineapple is not the "active" dynamic one). game.addChild(pineapple); Important: Ensure mergeCounter is not reset here. It only resets upon successful release of a dynamic pineapple.
User prompt
Objective 2: Flawless pineappleActive and mergeCounter Reset. Action for Ava: In releasePineappleOnMerge(): The condition should be if (mergeCounter >= GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED && !pineappleActive && pineapple). Inside this if block (when a pineapple IS released): pineappleActive = true; (Set this FIRST). (Perform visibility fixes as per Part 1). (Make pineapple dynamic, applyDropPhysics, add to fruits/spatialGrid). mergeCounter = 0; (Reset the counter immediately after deciding to release this pineapple). The LK.setTimeout is for the end of this dynamic pineapple's special phase: Inside the LK.setTimeout callback: if (currentReleasedPineapple && fruits.indexOf(currentReleasedPineapple) !== -1) { currentReleasedPineapple.immuneToGameOver = false; /* ... visibility assertions for currentReleasedPineapple ... */ } (Note: You'll need to capture the specific pineapple instance that was released, as the global pineapple will be replaced by setupPineapple()). pineappleActive = false; (The system is now ready for a new static pineapple). setupPineapple(); (Creates the new static pineapple for the next cycle). No pineappleActive = false; anywhere else in releasePineappleOnMerge().
User prompt
Objective 1: Strictly One Increment & Push per Merge. Action for Ava: Confirm: MergeComponent.completeMerge() is the only place that directly calls mergeCounter++; and then pushPineapple();. These should happen after the specific fruit's onMerge behavior (e.g., behaviorHandler.onMerge(...)) has finished. Remove mergeCounter++; and pushPineapple(); from all other locations (especially from releasePineappleOnMerge itself, and from individual fruit behavior onMerge functions like standardMerge or DURIAN.onMerge). MergeComponent.completeMerge() should then call releasePineappleOnMerge() after incrementing the counter and calling the push animation.
User prompt
Objective: The dynamic pineapple must remain fully visible from the moment it's released until it's removed from the game. Action for Ava: In releasePineappleOnMerge(): Inside the if (mergeCounter >= GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE && !pineappleActive && pineapple) block (this condition should use PINEAPPLE_MERGES_NEEDED, not CHARGE_NEEDED_FOR_RELEASE for the pineapple): Before applyDropPhysics(pineapple, 1.5);: Add: // --- AGGRESSIVE VISIBILITY FIX --- if (pineapple) { // Ensure pineapple exists tween.stop(pineapple); // Stop ALL existing tweens on this specific pineapple instance pineapple.alpha = 1.0; pineapple.visible = true; if (pineapple.scale && typeof pineapple.scale.set === 'function') { pineapple.scale.set(1.0); } else { pineapple.scaleX = 1.0; pineapple.scaleY = 1.0; } // Also iterate through direct children (the visual asset) if (pineapple.children && pineapple.children.length > 0) { for (var cIdx = 0; cIdx < pineapple.children.length; cIdx++) { var child = pineapple.children[cIdx]; tween.stop(child); // Stop tweens on children too child.alpha = 1.0; child.visible = true; if (child.scale && typeof child.scale.set === 'function') { child.scale.set(1.0); } else { child.scaleX = 1.0; child.scaleY = 1.0; } } } } // --- END AGGRESSIVE VISIBILITY FIX --- Use code with caution. JavaScript In the LK.setTimeout callback inside releasePineappleOnMerge(): When pineapple.immuneToGameOver = false; is set, also re-apply the same aggressive visibility fix block from above to the pineapple instance that is having its immunity removed. This ensures that even if something happened during the immunity period, it's made visible again.
User prompt
make sure this happens to fix the invisible pineapple. Simplified Pineapple Logic Flow (After Fixes): Game Starts (initGame) / Previous Pineapple Cycle Ends: setupPineapple() is called: creates a new, static, visible pineapple instance. Sets pineappleActive = false;. mergeCounter is 0. A Merge Occurs (any type): MergeComponent.completeMerge() is the final step. It calls the specific fruit's onMerge behavior. Then it does: mergeCounter++; pushPineapple(); (This function now only tweens the x position of the current static pineapple based on mergeCounter and GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED, but only if !pineappleActive && pineapple). releasePineappleOnMerge(); releasePineappleOnMerge() is Called: It checks: if (mergeCounter >= GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED && !pineappleActive && pineapple) If true: pineappleActive = true; Make current pineapple dynamic, visible, and apply physics. Add to fruits array and spatialGrid. LK.setTimeout is set: Callback: pineapple.immuneToGameOver = false; (ensure visibility again), pineappleActive = false;, setupPineapple(); (for the next one), mergeCounter = 0;. If false: Does nothing further. pushPineapple() Function: if (!pineappleActive && pineapple): Calculates newX based on mergeCounter. Tweens pineapple.x to newX. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Ensure setupPineapple() Creates a Visible Static Pineapple. Action for Ava: In setupPineapple(): When the new pineapple = new Fruit(FruitTypes.PINEAPPLE); is created, ensure its initial properties are correct for a static, visible element: pineapple.isStatic = true; pineapple.alpha = 1.0; pineapple.visible = true; pineapple.scale.set(1.0); // Ensure children are also visible if applicable if (pineapple.children && pineapple.children.length > 0) { for (var c = 0; c < pineapple.children.length; c++) { pineapple.children[c].alpha = 1.0; pineapple.children[c].visible = true; pineapple.children[c].scale.set(1.0); } } Use code with caution. JavaScript Set pineappleActive = false; here as well, because when setupPineapple is called (e.g., during initGame or after a previous pineapple's cycle ends), the system is indeed ready for a new static pineapple, and no dynamic one is active.
User prompt
Correct pineappleActive State Management for Release and Reset. Action for Ava: In releasePineappleOnMerge(): The condition if (mergeCounter >= GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED && !pineappleActive && pineapple) is the gate for releasing the dynamic pineapple. Inside this if block (when a pineapple IS released): Set pineappleActive = true; (This is crucial: the pineapple is now considered active). (The code to make it dynamic: isStatic = false, applyDropPhysics, add to fruits/spatialGrid is already here and correct). The LK.setTimeout callback is where the current active pineapple's cycle ends. Inside the LK.setTimeout callback: if (pineapple && fruits.indexOf(pineapple) !== -1) { pineapple.immuneToGameOver = false; /* ... ensure visibility as per Part 1 ... */ } Crucially, set: pineappleActive = false; (The dynamic pineapple is no longer the "active special feature"; the system can now prepare for the next one). Then, call: setupPineapple(); (This creates a new static pineapple, ready to be pushed by subsequent merges). And reset: mergeCounter = 0; (The count for this just-released pineapple is done). Remove pineappleActive = false; from the very end of the releasePineappleOnMerge() function if it's still there. This flag should only change based on the specific conditions above.
User prompt
One mergeCounter Increment and One pushPineapple() Call Per Completed Merge. Action for Ava: Centralize in MergeComponent.completeMerge(): This function is called after any type of merge (standard or Durian, as Durian's onMerge doesn't create a new fruit but is still a completed merge). Locate MergeComponent.completeMerge(). Ensure these two lines are present and are executed exactly once after the specific fruit's onMerge behavior has completed but before old fruits are removed: // Inside MergeComponent.completeMerge(), after behaviorHandler.onMerge(...) mergeCounter++; pushPineapple(); // Animate the static pineapple Use code with caution. JavaScript The call to releasePineappleOnMerge() should follow these, still within completeMerge. Remove from other locations: In releasePineappleOnMerge(): Remove mergeCounter++; and pushPineapple(); from the beginning of this function. In FruitBehavior.standardMerge(): Remove any calls to mergeCounter++, pushPineapple(), or releasePineappleOnMerge(). In FruitBehavior.DURIAN.onMerge(): Remove the call to releasePineappleOnMerge(). (The call in MergeComponent.completeMerge will now handle this path).
User prompt
Inside releasePineappleOnMerge() function: Within the if (mergeCounter >= GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED && !pineappleActive && pineapple) block, immediately before calling applyDropPhysics(pineapple, 1.5): Add lines to explicitly set the pineapple's visibility properties to their default active state: pineapple.alpha = 1.0; pineapple.visible = true; pineapple.scale.set(1.0); // Or pineapple.scaleX = 1; pineapple.scaleY = 1; // If the pineapple sprite has its own children that might also be invisible: if (pineapple.children && pineapple.children.length > 0) { for (var c = 0; c < pineapple.children.length; c++) { pineapple.children[c].alpha = 1.0; pineapple.children[c].visible = true; pineapple.children[c].scale.set(1.0); } } Use code with caution. JavaScript Inside the LK.setTimeout callback within releasePineappleOnMerge(): After setting pineapple.immuneToGameOver = false;, add the same block of code as above to re-assert its visibility and scale, just in case any intermediate state changes affected them. This ensures that when its immunity ends, it's definitely visible.
Code edit (2 edits merged)
Please save this source code
User prompt
the first few merges, actually move the pineapple to the right instead of pushing it to the right. the push should be relative to it's starting position, always, so set a starting x position for it, aand all subsequent pushes must push it to the right
Code edit (1 edits merged)
Please save this source code
Code edit (5 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); // wallContactFrames is now primarily for tracking if there *is* contact, // rather than complex progressive friction which has been simplified. 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; // Simplified effective dimension calculation - can be refined if necessary 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 }; // Reset contacts each frame before checking 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 isInContactWithBoundary = fruit._boundaryContacts.left || fruit._boundaryContacts.right || fruit._boundaryContacts.floor; if (isInContactWithBoundary) { self.wallContactFrames = Math.min(self.wallContactFrames + 1, GAME_CONSTANTS.MAX_WALL_CONTACT_FRAMES_FOR_FRICTION_EFFECT); // Cap frames for effect // Apply a consistent friction/damping if in contact with wall/floor // This is now handled more directly in PhysicsComponent based on contact flags } else { self.wallContactFrames = Math.max(0, self.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; // Remove bounce: set vx to 0 and apply friction instead of reversing velocity fruit.vx = 0; if (Math.abs(incomingVx) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) { LK.getSound('bounce').play(); } // Remove angular impulse from wall collision, but keep a little damping for realism fruit.angularVelocity *= GAME_CONSTANTS.WALL_ANGULAR_DAMPING; fruit._boundaryContacts.left = true; } }; 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; // Remove bounce: set vx to 0 and apply friction instead of reversing velocity fruit.vx = 0; if (Math.abs(incomingVx) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) { LK.getSound('bounce').play(); } // Remove angular impulse from wall collision, but keep a little damping for realism fruit.angularVelocity *= GAME_CONSTANTS.WALL_ANGULAR_DAMPING; 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 * GAME_CONSTANTS.FLOOR_BOUNCE_DAMPING; if (Math.abs(incomingVy) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) { LK.getSound('bounce').play(); } fruit._boundaryContacts.floor = true; // Apply friction to linear velocity when on floor (more direct than before) fruit.vx *= GAME_CONSTANTS.GROUND_LINEAR_FRICTION * 0.9; // Increase friction // Extra: If vx is very small, apply stronger friction to help it stop if (Math.abs(fruit.vx) < 0.05) { fruit.vx *= 0.7; // Stronger friction } // Simpler angular velocity effect from floor contact (rolling) if (Math.abs(fruit.vx) > GAME_CONSTANTS.MIN_VX_FOR_ROLLING) { // A small portion of vx is converted to angularVelocity to simulate rolling var targetAngular = fruit.vx * GAME_CONSTANTS.GROUND_ROLLING_FACTOR; // Blend towards target rolling angular velocity fruit.angularVelocity = fruit.angularVelocity * 0.7 + targetAngular * 0.3; } // Increase ground angular damping for more rapid spin decay fruit.angularVelocity *= 0.80; // Stronger than before // Extra: If angular velocity is very small, apply stronger friction to help it stop if (Math.abs(fruit.angularVelocity) < 0.01) { fruit.angularVelocity *= 0.5; // Even stronger friction } // Simplified resting condition if (Math.abs(fruit.vy) < GAME_CONSTANTS.RESTING_VELOCITY_THRESHOLD) { fruit.vy = 0; } if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.ANGULAR_RESTING_THRESHOLD) { 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; 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; } }; // Call initialize in the constructor pattern self.initialize(); 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); // Call parent's destroy }; 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; // Components are instantiated here var physics = new PhysicsComponent(); // Manages physics updates, stabilization, sleeping var collision = new CollisionComponent(); // Manages boundary collisions var mergeHandler = new MergeComponent(); // Manages merging logic var behaviorSystem = new FruitBehavior(); // Manages type-specific behaviors // Physics properties are on the fruit itself, managed by PhysicsComponent self.vx = 0; self.vy = 0; self.rotation = 0; self.angularVelocity = 0; var currentLevel = getFruitLevel(self); // Helper to get fruit level self.gravity = GAME_CONSTANTS.BASE_GRAVITY * (1 + Math.pow(currentLevel, GAME_CONSTANTS.GRAVITY_LEVEL_POWER_SCALE) * GAME_CONSTANTS.GRAVITY_LEVEL_MULTIPLIER_ADJUSTED); // Elasticity: Higher level fruits are slightly less bouncy self.elasticity = Math.max(GAME_CONSTANTS.MIN_ELASTICITY, GAME_CONSTANTS.BASE_ELASTICITY - currentLevel * GAME_CONSTANTS.ELASTICITY_DECREASE_PER_LEVEL); // State flags self.isStatic = false; // If true, physics is not applied (e.g., next fruit display) self.isSleeping = false; // If true, physics updates are minimal (already resting) self.isFullyStabilized = false; // Intermediate state for coming to rest self._sleepCounter = 0; // Counter for consecutive frames with low energy to trigger sleeping self.wallContactFrames = 0; // Now simpler, tracked by CollisionComponent but can be read by Fruit if needed // Merge related properties self.merging = false; // True if this fruit is currently in a merge animation self.mergeGracePeriodActive = false; // Short period after drop where merges are prioritized self.fromChargedRelease = false; // If this fruit spawned from the special charge release // Game logic flags self.safetyPeriod = false; // Used in game over logic (original logic retained) self.immuneToGameOver = false; // If true, this fruit cannot trigger game over // Behavior (e.g., special merge effects) 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.warn("Fruit: Type not available or missing required properties for fruit ID: " + (type ? type.id : 'unknown')); } // Public methods to interact with components self.updatePhysics = function () { // Delegate to the physics component, passing self (the fruit instance) physics.apply(self); }; self.checkBoundaries = function (walls, floor) { // Delegate to the collision component collision.checkBoundaryCollisions(self, walls, floor); self.wallContactFrames = collision.wallContactFrames; // Update fruit's copy if needed elsewhere }; self.merge = function (otherFruit) { // Delegate to the merge handler mergeHandler.beginMerge(self, otherFruit); }; // Method to wake up a fruit if it's sleeping or stabilized self.wakeUp = function () { if (self.isSleeping || self.isFullyStabilized) { self.isSleeping = false; self.isFullyStabilized = false; self._sleepCounter = 0; // Optional: apply a tiny impulse if needed to ensure it starts moving // self.vy -= 0.1; } }; 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(); // Assumes this global function exists removeFruitFromGame(f1); // Assumes this global function exists removeFruitFromGame(f2); // Assumes this global function exists return null; // No new fruit created from Durian merge } } }; self.getMergeHandler = function (fruitTypeId) { if (!fruitTypeId) { console.warn("FruitBehavior: fruitTypeId is null or undefined in getMergeHandler. Defaulting to CHERRY."); return self.behaviors.CHERRY; } var upperTypeId = fruitTypeId.toUpperCase(); return self.behaviors[upperTypeId] || self.behaviors.CHERRY; }; self.standardMerge = function (fruit1, fruit2, posX, posY) { var nextFruitKey = fruit1.type.next; if (!nextFruitKey) { console.error("FruitBehavior: 'next' type is undefined for fruit type: " + fruit1.type.id); return null; // Cannot create next level fruit } var nextType = FruitTypes[nextFruitKey.toUpperCase()]; if (!nextType) { console.error("FruitBehavior: Next fruit type '" + nextFruitKey + "' not found in FruitTypes."); return null; } var newFruit = self.createNextLevelFruit(fruit1, nextType, posX, posY); return newFruit; }; self.createNextLevelFruit = function (sourceFruit, nextType, posX, posY) { var newFruit = new Fruit(nextType); // Fruit class constructor newFruit.x = posX; newFruit.y = posY; // Initial scale for merge animation newFruit.scaleX = 0.5; newFruit.scaleY = 0.5; game.addChild(newFruit); // Assumes 'game' is a global Application or Container fruits.push(newFruit); // Assumes 'fruits' is a global array if (spatialGrid) { // Assumes 'spatialGrid' is a global instance spatialGrid.insertObject(newFruit); } LK.setScore(LK.getScore() + nextType.points); updateScoreDisplay(); // Assumes this global function exists tween(newFruit, { scaleX: 1, scaleY: 1 }, { duration: 300, // Standard merge pop-in duration 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', { // Re-using floor asset for the line anchorX: 0.5, anchorY: 0.5 }); lineGraphics.tint = 0xff0000; // Red color for game over line lineGraphics.height = 20; // Make it visually a line // Width will be scaled in setupBoundaries return self; }); var MergeComponent = Container.expand(function () { var self = Container.call(this); self.merging = false; // This property seems to belong to the Fruit itself, not the component instance. // Each fruit will have its own 'merging' flag. self.fruitBehavior = new FruitBehavior(); // To get specific merge behaviors self.beginMerge = function (fruit1, fruit2) { // Prevent merging if either fruit is already involved in a merge if (fruit1.merging || fruit2.merging) { return; } // Prevent merging if fruit types are different (should be handled by collision check before calling this) if (fruit1.type.id !== fruit2.type.id) { console.warn("MergeComponent: Attempted to merge fruits of different types. This should be checked earlier."); return; } fruit1.merging = true; fruit2.merging = true; var midX = (fruit1.x + fruit2.x) / 2; var midY = (fruit1.y + fruit2.y) / 2; self.animateMerge(fruit1, fruit2, midX, midY); }; self.animateMerge = function (fruit1, fruit2, midX, midY) { // Animate fruit1 shrinking and fading tween(fruit1, { alpha: 0, scaleX: 0.5, scaleY: 0.5 // Optional: Move towards merge point if desired, though often they are already close // x: midX, // y: midY }, { duration: GAME_CONSTANTS.MERGE_ANIMATION_DURATION, // Use a constant easing: tween.easeOut }); // Animate fruit2 shrinking and fading, then complete the merge tween(fruit2, { alpha: 0, scaleX: 0.5, scaleY: 0.5 // x: midX, // y: midY }, { duration: GAME_CONSTANTS.MERGE_ANIMATION_DURATION, easing: tween.easeOut, onFinish: function onFinish() { self.completeMerge(fruit1, fruit2, midX, midY); } }); }; self.completeMerge = function (fruit1, fruit2, midX, midY) { LK.getSound('merge').play(); self.trackMergeAnalytics(fruit1, fruit2); // For game analytics or special logic var behaviorHandler = self.fruitBehavior.getMergeHandler(fruit1.type.id); var newFruit = null; if (behaviorHandler && behaviorHandler.onMerge) { newFruit = behaviorHandler.onMerge(fruit1, fruit2, midX, midY); } else { // Fallback to standard merge if no specific behavior or handler is missing console.warn("MergeComponent: No specific onMerge behavior for " + fruit1.type.id + ", or handler missing. Attempting standard merge."); var nextFruitKey = fruit1.type.next; if (nextFruitKey) { var nextType = FruitTypes[nextFruitKey.toUpperCase()]; if (nextType) { newFruit = self.fruitBehavior.createNextLevelFruit(fruit1, nextType, midX, midY); } else { console.error("MergeComponent: Fallback standard merge failed, next fruit type '" + nextFruitKey + "' not found."); } } else { console.error("MergeComponent: Fallback standard merge failed, 'next' type is undefined for " + fruit1.type.id); } } // Centralized authoritative point for incrementing mergeCounter and calling pushPineapple mergeCounter++; pushPineapple(); // Check for pineapple release after merge is complete releasePineappleOnMerge(); // Clean up old fruits. This should happen *after* the new fruit is potentially created and added. // Durian behavior handles its own removal. if (fruit1.type.id.toUpperCase() !== 'DURIAN') { removeFruitFromGame(fruit1); // Global removal function } // Fruit2 is always removed unless it was part of a special non-creating merge (like Durian's partner) // The Durian logic already removes both. if (fruit2.type.id.toUpperCase() !== 'DURIAN' || fruit1.type.id.toUpperCase() !== 'DURIAN') { // If neither was a Durian, or if fruit1 was Durian (so fruit2 is its partner to be removed) removeFruitFromGame(fruit2); } }; // This function seems to be for tracking specific game states related to merges self.trackMergeAnalytics = function (fruit1, fruit2) { // Original logic for lastDroppedHasMerged - ensure lastDroppedFruit is a global variable 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); // Note: Properties like vx, vy, gravity, etc., are on the Fruit object itself. // This component's methods will modify those properties on the passed 'fruit' instance. self.apply = function (fruit) { if (fruit.isStatic || fruit.merging) { return; // No physics for static or merging fruits } // If fruit is sleeping, only perform minimal checks or wake it up if necessary. if (fruit.isSleeping) { // A sleeping fruit should not move. Velocities should already be zero. fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; // Potentially check for significant external forces that might wake it up (handled in collision response) return; } var prevVx = fruit.vx; var prevVy = fruit.vy; // 1. Apply Gravity // fruit.gravity is pre-calculated on fruit creation based on its level fruit.vy += fruit.gravity * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT; // Assuming a fixed timestep or adjusting for it // 2. Apply Air Friction / Damping (if not in contact with ground) if (!fruit._boundaryContacts || !fruit._boundaryContacts.floor) { // Check if fruit is resting on another fruit (not in air, not on floor/walls) var restingOnOtherFruit = false; if (fruit._boundaryContacts && !fruit._boundaryContacts.floor && !fruit._boundaryContacts.left && !fruit._boundaryContacts.right) { // If the fruit has neighborContacts, and at least one is directly below, consider it "resting on another fruit" if (fruit.neighborContacts && fruit.neighborContacts.length > 0) { for (var i = 0; i < fruits.length; i++) { var neighbor = fruits[i]; if (neighbor && neighbor.id && fruit.neighborContacts.indexOf(neighbor.id) !== -1) { // Check if neighbor is directly below (simple Y check, can be improved) if (neighbor.y > fruit.y && Math.abs(neighbor.x - fruit.x) < (fruit.width + neighbor.width) / 2) { restingOnOtherFruit = true; break; } } } } } fruit.vx *= GAME_CONSTANTS.AIR_FRICTION_MULTIPLIER; fruit.vy *= GAME_CONSTANTS.AIR_FRICTION_MULTIPLIER; // Air friction also affects vertical, less than gravity though fruit.angularVelocity *= GAME_CONSTANTS.AIR_ANGULAR_DAMPING; // Apply a small, constant angular damping every frame to gently slow down free-spinning in the air fruit.angularVelocity *= 0.995; // Extra angular friction in air to help stop spinning if (Math.abs(fruit.angularVelocity) < 0.01) { fruit.angularVelocity *= 0.90; // Increased friction } // Extra linear friction in air to help stop drifting if (Math.abs(fruit.vx) < 0.05) { fruit.vx *= 0.90; // Increased friction } if (Math.abs(fruit.vy) < 0.05) { fruit.vy *= 0.90; // Increased friction } // If resting on another fruit, apply extra friction to help it come to rest if (restingOnOtherFruit) { fruit.vx *= 0.85; // Stronger friction fruit.vy *= 0.85; fruit.angularVelocity *= 0.85; // If velocities are very small, damp even more if (Math.abs(fruit.vx) < 0.02) { fruit.vx *= 0.7; } if (Math.abs(fruit.vy) < 0.02) { fruit.vy *= 0.7; } if (Math.abs(fruit.angularVelocity) < 0.005) { fruit.angularVelocity *= 0.7; } } } else { // If on ground, specific ground friction is handled in CollisionComponent.checkFloorCollision // and here for angular damping. // Increase general ground contact angular damping fruit.angularVelocity *= 0.88; // Stronger damping than before // If fruit is on a surface (floor or wall) and linear velocity is low, apply even stronger angular damping var onSurface = fruit._boundaryContacts && (fruit._boundaryContacts.floor || fruit._boundaryContacts.left || fruit._boundaryContacts.right); if (onSurface && Math.abs(fruit.vx) < 0.08 && Math.abs(fruit.vy) < 0.08) { fruit.angularVelocity *= 0.7; // Very strong damping when nearly at rest on a surface } // Extra angular friction on ground to help stop spinning if (Math.abs(fruit.angularVelocity) < 0.01) { fruit.angularVelocity *= 0.75; // Even more friction } // Extra linear friction on ground to help stop sliding if (Math.abs(fruit.vx) < 0.05) { fruit.vx *= 0.80; // Increased friction } if (Math.abs(fruit.vy) < 0.05) { fruit.vy *= 0.80; // Increased friction } } // 3. Update Rotation fruit.rotation += fruit.angularVelocity * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT; self.handleRotationLimits(fruit); // Cap angular velocity and handle near-zero stopping // 4. Limit Velocities (Max Speeds) var maxLinearSpeed = GAME_CONSTANTS.MAX_LINEAR_VELOCITY_BASE - getFruitLevel(fruit) * GAME_CONSTANTS.MAX_LINEAR_VELOCITY_REDUCTION_PER_LEVEL; maxLinearSpeed = Math.max(GAME_CONSTANTS.MIN_MAX_LINEAR_VELOCITY, maxLinearSpeed); var currentSpeed = Math.sqrt(fruit.vx * fruit.vx + fruit.vy * fruit.vy); if (currentSpeed > maxLinearSpeed) { var ratio = maxLinearSpeed / currentSpeed; fruit.vx *= ratio; fruit.vy *= ratio; } // 5. Update Position fruit.x += fruit.vx * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT; fruit.y += fruit.vy * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT; // 6. Stabilization and Sleep Logic (Simplified) self.checkStabilization(fruit, prevVx, prevVy); // Ensure mid-air fruits are not marked as sleeping or stabilized if (!fruit._boundaryContacts || !fruit._boundaryContacts.floor && !fruit._boundaryContacts.left && !fruit._boundaryContacts.right) { fruit.isFullyStabilized = false; fruit.isSleeping = false; fruit._sleepCounter = 0; } // If the fruit is now fully stabilized (but not yet sleeping), force velocities to zero. if (fruit.isFullyStabilized && !fruit.isSleeping) { if (Math.abs(fruit.vx) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD && Math.abs(fruit.vy) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD && Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.STABLE_ANGULAR_VELOCITY_THRESHOLD) { // This is a soft "almost stopped" state, let's push it to fully stopped fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; } // Additional: If velocities are extremely small, always zero them out if (Math.abs(fruit.vx) < 0.005) { fruit.vx = 0; } if (Math.abs(fruit.vy) < 0.005) { fruit.vy = 0; } if (Math.abs(fruit.angularVelocity) < 0.0005) { fruit.angularVelocity = 0; } } // Ensure minimum fall speed if in air and moving downwards very slowly (prevents floating) if (fruit.vy > 0 && fruit.vy < GAME_CONSTANTS.MIN_FALL_SPEED_IF_SLOWING && (!fruit._boundaryContacts || !fruit._boundaryContacts.floor)) { fruit.vy = GAME_CONSTANTS.MIN_FALL_SPEED_IF_SLOWING; } }; self.handleRotationLimits = function (fruit) { // Cap angular velocity var maxAngVel = GAME_CONSTANTS.MAX_ANGULAR_VELOCITY_BASE - getFruitLevel(fruit) * GAME_CONSTANTS.MAX_ANGULAR_VELOCITY_REDUCTION_PER_LEVEL; maxAngVel = Math.max(GAME_CONSTANTS.MIN_MAX_ANGULAR_VELOCITY, maxAngVel); fruit.angularVelocity = Math.max(-maxAngVel, Math.min(maxAngVel, fruit.angularVelocity)); // If angular velocity is very low, and fruit is somewhat stable, stop rotation if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.ANGULAR_STOP_THRESHOLD_PHYSICS) { // Additional check: if linear velocity is also very low, it's more likely to stop rotating var linSpeedSq = fruit.vx * fruit.vx + fruit.vy * fruit.vy; if (linSpeedSq < GAME_CONSTANTS.LINEAR_SPEED_SQ_FOR_ANGULAR_STOP) { fruit.angularVelocity = 0; fruit.rotationRestCounter = (fruit.rotationRestCounter || 0) + 1; } else { fruit.rotationRestCounter = 0; } // Extra: Clamp to zero if extremely small if (Math.abs(fruit.angularVelocity) < 0.0002) { fruit.angularVelocity = 0; } } else { fruit.rotationRestCounter = 0; } }; self.checkStabilization = function (fruit, prevVx, prevVy) { // Calculate change in velocity (kinetic energy proxy) var deltaVSq = (fruit.vx - prevVx) * (fruit.vx - prevVx) + (fruit.vy - prevVy) * (fruit.vy - prevVy); var currentSpeedSq = fruit.vx * fruit.vx + fruit.vy * fruit.vy; var currentAngularSpeed = Math.abs(fruit.angularVelocity); // Conditions for considering a fruit "active" (not stabilizing) // Lowered thresholds for more decisive stabilization var isActive = currentSpeedSq > GAME_CONSTANTS.ACTIVE_LINEAR_SPEED_SQ_THRESHOLD * 0.7 || currentAngularSpeed > GAME_CONSTANTS.ACTIVE_ANGULAR_SPEED_THRESHOLD * 0.7 || deltaVSq > GAME_CONSTANTS.ACTIVE_DELTA_V_SQ_THRESHOLD * 0.7; // Fruit is considered potentially stabilizing if it's in contact with something (floor or walls) var onSurface = fruit._boundaryContacts && (fruit._boundaryContacts.floor || fruit._boundaryContacts.left || fruit._boundaryContacts.right); if (!isActive && onSurface) { fruit._sleepCounter = (fruit._sleepCounter || 0) + 1; } else { // If it becomes active or is in mid-air, reset counter and wake it up fruit._sleepCounter = 0; fruit.isSleeping = false; fruit.isFullyStabilized = false; // If active, it's not stabilized } if (fruit._sleepCounter >= GAME_CONSTANTS.SLEEP_DELAY_FRAMES_SIMPLIFIED + 8) { // If it has been inactive for enough frames (slightly longer to allow for jostle) if (!fruit.isSleeping) { // Only set to sleeping if not already fruit.isSleeping = true; fruit.isFullyStabilized = true; // Sleeping implies fully stabilized fruit.vx = 0; fruit.vy = 0; fruit.angularVelocity = 0; } } else if (fruit._sleepCounter >= GAME_CONSTANTS.STABILIZED_DELAY_FRAMES) { // If inactive for a shorter period, it's "fully stabilized" but not yet sleeping // This state can be used for game logic like game over checks if (!fruit.isFullyStabilized && !fruit.isSleeping) { fruit.isFullyStabilized = true; } // More aggressively zero out velocities if below very small thresholds if (Math.abs(fruit.vx) < 0.003) { fruit.vx = 0; } if (Math.abs(fruit.vy) < 0.003) { fruit.vy = 0; } if (Math.abs(fruit.angularVelocity) < 0.0002) { fruit.angularVelocity = 0; } } else { // If not meeting sleep or stabilized counts, it's not in these states fruit.isSleeping = false; fruit.isFullyStabilized = false; } }; return self; }); var SpatialGrid = Container.expand(function (cellSize) { var self = Container.call(this); self.cellSize = cellSize || 200; // Default cell size self.grid = {}; // Stores cellKey: [object1, object2, ...] self.lastRebuildTime = Date.now(); self.rebuildInterval = GAME_CONSTANTS.SPATIAL_GRID_REBUILD_INTERVAL_MS; // How often to rebuild if objects don't move self.insertObject = function (obj) { if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number' || obj.merging || obj.isStatic) { return; // Skip invalid, merging, or static objects } obj._currentCells = obj._currentCells || []; // Ensure property exists var cells = self.getCellsForObject(obj); obj._currentCells = cells.slice(); // Store the cells the object is currently in for (var i = 0; i < cells.length; i++) { var cellKey = cells[i]; if (!self.grid[cellKey]) { self.grid[cellKey] = []; } // Avoid duplicates if somehow inserted twice if (self.grid[cellKey].indexOf(obj) === -1) { self.grid[cellKey].push(obj); } } }; self.removeObject = function (obj) { if (!obj || !obj._currentCells) { // Check if obj is valid and has _currentCells return; } var cells = obj._currentCells; // Use stored cells for removal efficiency for (var i = 0; i < cells.length; i++) { var cellKey = cells[i]; if (self.grid[cellKey]) { var cellIndex = self.grid[cellKey].indexOf(obj); if (cellIndex !== -1) { self.grid[cellKey].splice(cellIndex, 1); } // Clean up empty cell arrays if (self.grid[cellKey].length === 0) { delete self.grid[cellKey]; } } } obj._currentCells = []; // Clear stored cells }; // Calculates which grid cells an object overlaps with self.getCellsForObject = function (obj) { if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number') { return []; // Return empty array for invalid input } var cells = []; // Using object's center and dimensions to find cell range 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); // Unique key for each cell } } return cells; }; self.updateObject = function (obj) { if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number' || obj.merging || obj.isStatic) { // Don't update static or merging objects in the grid return; } var newCells = self.getCellsForObject(obj); var oldCells = obj._currentCells || []; // Efficiently check if cells have changed var cellsChanged = false; if (oldCells.length !== newCells.length) { cellsChanged = true; } else { // Check if all new cells are in oldCells (and vice-versa implied by same length) for (var i = 0; i < newCells.length; i++) { if (oldCells.indexOf(newCells[i]) === -1) { cellsChanged = true; break; } } } if (cellsChanged) { self.removeObject(obj); // Remove from old cells self.insertObject(obj); // Insert into new cells } }; // Gets potential collision candidates for an object self.getPotentialCollisions = function (obj) { var candidates = []; if (!obj || typeof obj.x !== 'number') { return candidates; } // Basic check for valid object var cells = self.getCellsForObject(obj); // Determine cells the object is in var addedObjects = {}; // To prevent duplicate candidates if an object is in multiple shared cells 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]; // Ensure otherObj is valid, not the same as obj, and not already added if (otherObj && otherObj !== obj && !addedObjects[otherObj.id]) { if (otherObj.merging || otherObj.isStatic) { continue; } // Skip merging or static candidates candidates.push(otherObj); addedObjects[otherObj.id] = true; // Mark as added } } } } return candidates; }; self.clear = function () { self.grid = {}; self.lastRebuildTime = Date.now(); }; // Rebuilds the entire grid. Useful if many objects become invalid or after a reset. self.rebuildGrid = function (allObjects) { self.grid = {}; // Clear current grid self.lastRebuildTime = Date.now(); if (Array.isArray(allObjects)) { for (var i = 0; i < allObjects.length; i++) { // Only insert valid, non-merging, non-static objects 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(30); // Reduced default pool size, can grow self.activeDots = []; // DisplayObjects currently on stage // self.dots renamed to activeDots for clarity self.dotSpacing = 25; // Spacing between dots self.dotSize = 15; // Visual size (not used in current createObject) // Dynamically calculate maxDots so the trajectory can reach from drop point to floor // This is recalculated in case drop point or floor changes function calculateMaxDots() { var dropActualY = GAME_CONSTANTS.DROP_POINT_Y + GAME_CONSTANTS.DROP_START_Y_OFFSET; var distanceToCover = GAME_CONSTANTS.GAME_HEIGHT - dropActualY; var requiredDots = Math.ceil(distanceToCover / self.dotSpacing) + 5; // +5 for a little buffer if (requiredDots <= 0) { requiredDots = 60; } // Fallback if calculation is off return requiredDots; } self.maxDots = calculateMaxDots(); self.createDots = function () { // Renamed from initialize for clarity self.clearDots(); // Clear existing before creating new pool (if any) // DotPool's initialize is called by its constructor if initialSize is given }; self.clearDots = function () { while (self.activeDots.length > 0) { var dot = self.activeDots.pop(); if (dot) { self.removeChild(dot); // Remove from PIXI stage self.dotPool.release(dot); // Return to pool } } }; self.updateTrajectory = function (startX, startY) { if (!activeFruit) { self.clearDots(); return; } self.clearDots(); // Start the trajectory line at the correct offset below the fruit's bottom var currentY = startY; var dotCount = 0; var hitDetected = false; var currentFruitRadius = activeFruit.width / 2; var COLLISION_CHECK_INTERVAL = 1; // Check every dot for best accuracy // Precompute floor collision Y for this fruit var floorCollisionY = gameFloor.y - gameFloor.height / 2 - currentFruitRadius; // We'll keep track of the first intersection (floor or fruit) var firstHitY = null; var firstHitType = null; // "floor" or "fruit" var firstHitFruit = null; // Always use a fixed spacing between dots, regardless of how far the line goes var maxDistance = floorCollisionY - startY; var maxDotsByDistance = Math.ceil(Math.abs(maxDistance) / self.dotSpacing) + 5; var maxDots = Math.min(self.maxDots, maxDotsByDistance); for (dotCount = 0; dotCount < maxDots && !hitDetected; dotCount++) { var dot = self.dotPool.get(); if (!dot) { continue; } self.addChild(dot); self.activeDots.push(dot); dot.x = startX; dot.y = startY + dotCount * self.dotSpacing; dot.visible = true; dot.alpha = 1.0 - dotCount / self.maxDots * 0.7; dot.scale.set(1.0 - dotCount / self.maxDots * 0.5); var thisY = dot.y; // Check for floor collision if (thisY > floorCollisionY && !hitDetected) { firstHitY = floorCollisionY; firstHitType = "floor"; hitDetected = true; break; } // Check for fruit collisions if (dotCount % COLLISION_CHECK_INTERVAL === 0 && !hitDetected) { var trajectoryCheckObject = { x: startX, y: thisY, width: activeFruit.width, height: activeFruit.height, id: 'trajectory_check_point' }; var potentialHits = spatialGrid.getPotentialCollisions(trajectoryCheckObject); for (var j = 0; j < potentialHits.length; j++) { var fruit = potentialHits[j]; if (fruit && fruit !== activeFruit && !fruit.merging && !fruit.isStatic && fruit.width && fruit.height && fruit._boundaryContacts && fruit._boundaryContacts.floor // Only landed fruits ) { if (self.ellipseEllipseIntersect(startX, thisY, activeFruit.width, activeFruit.height, fruit.x, fruit.y, fruit.width, fruit.height)) { // Found a hit with a fruit firstHitY = thisY; firstHitType = "fruit"; firstHitFruit = fruit; hitDetected = true; break; } } } } if (hitDetected) { break; } } // If we hit something, adjust the last dot to be exactly at the intersection if (hitDetected && self.activeDots.length > 0) { var lastDot = self.activeDots[self.activeDots.length - 1]; if (firstHitType === "floor") { lastDot.y = floorCollisionY; } else if (firstHitType === "fruit" && firstHitFruit) { // Place the dot just above the hit fruit, using ellipse radii var yOffset = firstHitFruit.height / 2 + activeFruit.height / 2 + 2; lastDot.y = firstHitFruit.y - yOffset; // Clamp to floor if needed if (lastDot.y > floorCollisionY) { lastDot.y = floorCollisionY; } } // Hide any extra dots after the intersection for (var i = self.activeDots.length; i < self.maxDots; i++) { var extraDot = self.dotPool.get(); if (extraDot) { extraDot.visible = false; self.dotPool.release(extraDot); } } } }; // Ellipse-ellipse intersection for trajectory prediction self.ellipseEllipseIntersect = function (x1, y1, w1, h1, x2, y2, w2, h2) { // If both ellipses are circles, use circle distance if (Math.abs(w1 - h1) < 1 && Math.abs(w2 - h2) < 1) { var r1 = w1 / 2; var r2 = w2 / 2; var dx = x1 - x2; var dy = y1 - y2; return dx * dx + dy * dy <= (r1 + r2) * (r1 + r2); } // For general ellipses, use a quick approximation: // Project the distance between centers onto the axis, normalize by radii var dx = x1 - x2; var dy = y1 - y2; var rx = w1 / 2 + w2 / 2; var ry = h1 / 2 + h2 / 2; // Normalize the distance var norm = dx * dx / (rx * rx) + dy * dy / (ry * ry); return norm <= 1.0; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ // No title, no description backgroundColor: 0xffe122 }); /**** * Game Code ****/ // Global variable for trajectory line Y offset (relative to fruit's bottom, not its center) // --- Constants --- function getTrajectoryLineYOffset(fruit) { // Offset is from the bottom of the fruit, so half the height plus a small gap (e.g. 50px) return (fruit && fruit.height ? fruit.height / 2 : 0) + 10; } var _GAME_CONSTANTS; // Keep original babel helper structure if present function _typeof(o) { /* Babel helper */"@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _defineProperty(e, r, t) { /* Babel helper */ return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { /* Babel helper */ var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { /* Babel helper */ if ("object" != _typeof(t) || !t) { return t; } var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) { return i; } throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } // Refactored and added constants var GAME_CONSTANTS = _GAME_CONSTANTS = { GAME_WIDTH: 2048, GAME_HEIGHT: 2732, DROP_POINT_Y: 200, DROP_START_Y_OFFSET: 200, // Initial offset above the drop point for visual placement CLICK_DELAY_MS: 200, // Reduced for responsiveness FRUIT_IMMUNITY_MS: 300, // Shorter immunity after drop MERGE_GRACE_MS: 100, // Shorter grace period for merges MERGE_ANIMATION_DURATION: 150, // ms for merge shrink animation GAME_OVER_LINE_Y: 550, GAME_OVER_COUNTDOWN_MS: 2500, // Slightly shorter countdown // --- Simplified Physics Constants --- BASE_GRAVITY: 2, // Increased gravity for faster falling fruits // Adjusted for a different feel, may need tuning with TIMESTEP GRAVITY_LEVEL_POWER_SCALE: 1.25, // How much fruit level affects its mass/gravity effect (power) GRAVITY_LEVEL_MULTIPLIER_ADJUSTED: 0.1, // Additional multiplier for level-based gravity adjustment PHYSICS_TIMESTEP_ADJUSTMENT: 1.0, // If using fixed update, this is 1. If variable, use delta time. For now, 1. BASE_ELASTICITY: 0.8, // Base bounciness MIN_ELASTICITY: 0.1, // Minimum bounciness for heaviest fruits ELASTICITY_DECREASE_PER_LEVEL: 0.05, // How much elasticity decreases per fruit level AIR_FRICTION_MULTIPLIER: 0.99, // General damping for linear velocity in air (per frame) GROUND_LINEAR_FRICTION: 0.92, // Friction for linear velocity when on ground (per frame by CollisionComponent) AIR_ANGULAR_DAMPING: 0.98, // Damping for angular velocity in air GROUND_CONTACT_ANGULAR_DAMPING: 0.92, // General angular damping when on ground GROUND_ANGULAR_DAMPING: 0.85, // Specific angular damping from floor contact (CollisionComponent) WALL_BOUNCE_DAMPING: 0.85, // How much velocity is retained on wall bounce (multiplied by elasticity) FLOOR_BOUNCE_DAMPING: 0.75, // How much velocity is retained on floor bounce WALL_ANGULAR_IMPULSE_FACTOR: 0, // Small rotation from hitting walls WALL_ANGULAR_DAMPING: 0.9, // Damping of angular velocity after wall hit GROUND_ROLLING_FACTOR: 0.004, // Lowered to reduce induced spin from rolling // How much horizontal speed contributes to rolling on ground MIN_VX_FOR_ROLLING: 0.13, // Slightly increased to reduce jitter and unnecessary rolling // Minimum horizontal speed to initiate rolling effect MAX_LINEAR_VELOCITY_BASE: 50, MAX_LINEAR_VELOCITY_REDUCTION_PER_LEVEL: 2.0, MIN_MAX_LINEAR_VELOCITY: 10, MAX_ANGULAR_VELOCITY_BASE: 0.12, // Radians per frame MAX_ANGULAR_VELOCITY_REDUCTION_PER_LEVEL: 0.005, MIN_MAX_ANGULAR_VELOCITY: 0.03, ANGULAR_STOP_THRESHOLD_PHYSICS: 0.0008, // If angular speed is below this, might stop (in PhysicsComponent) LINEAR_SPEED_SQ_FOR_ANGULAR_STOP: 0.05, // If linear speed squared is also low, rotation fully stops RESTING_VELOCITY_THRESHOLD: 0.4, // Below this vy on floor, vy becomes 0 (in CollisionComponent) ANGULAR_RESTING_THRESHOLD: 0.005, // Below this angularV on floor, angularV becomes 0 (in CollisionComponent) MAX_WALL_CONTACT_FRAMES_FOR_FRICTION_EFFECT: 10, // Max frames wall contact has minor effect (used by CollisionComponent state) // Simplified Sleep/Stabilization State Constants SLEEP_DELAY_FRAMES_SIMPLIFIED: 55, // Frames of low activity to go to sleep (increased for more robust settling) STABILIZED_DELAY_FRAMES: 20, // Frames of low activity to be considered "fully stabilized" (for game over, etc.) ACTIVE_LINEAR_SPEED_SQ_THRESHOLD: 0.07, // If linear speed squared is above this, fruit is active ACTIVE_ANGULAR_SPEED_THRESHOLD: 0.0035, // If angular speed is above this, fruit is active ACTIVE_DELTA_V_SQ_THRESHOLD: 0.05, // If change in velocity squared is high, fruit is active STABLE_VELOCITY_THRESHOLD: 0.03, // If velocities are below this when 'isFullyStabilized', they are zeroed out. STABLE_ANGULAR_VELOCITY_THRESHOLD: 0.0007, MIN_FALL_SPEED_IF_SLOWING: 0.05, // Prevents fruits from floating if gravity effect becomes too small mid-air WAKE_UP_IMPULSE_THRESHOLD_LINEAR: 0.5, // Min linear impulse from collision to wake up a sleeping fruit WAKE_UP_IMPULSE_THRESHOLD_ANGULAR: 0.01, // Min angular impulse from collision to wake up // Collision Response FRUIT_COLLISION_SEPARATION_FACTOR: 0.9, // How much to push fruits apart on overlap (0 to 1) FRUIT_COLLISION_BASE_MASS_POWER: 1.5, // Fruit mass = level ^ this_power (for impulse calc) FRUIT_COLLISION_FRICTION_COEFFICIENT: 0.01, // Tangential friction between fruits FRUIT_COLLISION_ROTATION_TRANSFER: 0.005, // How much tangential collision affects rotation (keep small) FRUIT_HITBOX_REDUCTION_PER_LEVEL_DIFF: 1.5, // For `getAdjustedFruitRadius` BOUNCE_SOUND_VELOCITY_THRESHOLD: 1.0, // Adjusted threshold for playing bounce sound // Spatial Grid SPATIAL_GRID_REBUILD_INTERVAL_MS: 30000, // Less frequent full rebuilds SPATIAL_GRID_CELL_SIZE_FACTOR: 1.0, // Base cell size on average fruit size // UI & Game Features CHARGE_NEEDED_FOR_RELEASE: 15, PORTAL_UI_Y: 120, PORTAL_UI_X_OFFSET: 870, PORTAL_TWEEN_DURATION: 300, PORTAL_PULSE_DURATION: 500, PINEAPPLE_START_Y: 200, PINEAPPLE_END_POS_FACTOR: 0.16, PINEAPPLE_TWEEN_DURATION: 300, PINEAPPLE_IMMUNITY_MS: 2000, // Slightly shorter FIRE_BASE_COUNT: 3, FIRE_FRUIT_TYPE_THRESHOLD: 1, FIRE_MAX_COUNT: 15, FIRE_START_Y_OFFSET: 50, FIRE_STACK_Y_OFFSET: 100, FIRE_X_SPREAD: 500, FIRE_FLICKER_SPEED_BASE: 500, FIRE_FLICKER_SPEED_RANDOM: 300, FIRE_ALPHA_MIN: 0.1, FIRE_ALPHA_MAX: 0.5, FIRE_FRAME_DURATION: 200, COCONUT_SPAWN_SCORE_INTERVAL: 500, EVOLUTION_LINE_Y: 120, EVOLUTION_ICON_MAX_SIZE: 150, // Adjusted for potentially smaller screen area EVOLUTION_ICON_SPACING: 15, SCORE_TEXT_Y: 400, // Adjusted for consistency if needed SCORE_TEXT_SIZE: 120 // Adjusted for consistency }; // Removed the comma and the comment "/* Babel definitions if any */" // --- Game Variables --- var gameOverLine; var pineapple; // The special pineapple fruit instance var pineappleActive = false; // If the special pineapple has been dropped // var pineapplePushCount = 0; // This seems to be replaced by mergeCounter for pineapple logic var trajectoryLine; // Instance of TrajectoryLine class var isClickable = true; // Prevents rapid firing of fruits var evolutionLine; // UI element showing fruit progression var fireContainer; // Container for fire particle effects var activeFireElements = []; // Array of active fire Particle instances var fruitLevels = { // Defines the "level" or tier of each fruit type 'CHERRY': 1, 'GRAPE': 2, 'APPLE': 3, 'ORANGE': 4, 'WATERMELON': 5, 'PINEAPPLE': 6, 'MELON': 7, 'PEACH': 8, 'COCONUT': 9, 'DURIAN': 10 }; var FruitTypes = { // Defines properties for each fruit 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' }, // Note: Orange size was smaller than apple 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 } // No next fruit after Durian }; var gameWidth = GAME_CONSTANTS.GAME_WIDTH; var gameHeight = GAME_CONSTANTS.GAME_HEIGHT; var fruits = []; // Array to hold all active Fruit instances in the game var nextFruitType = null; // The type of the next fruit to be dropped var activeFruit = null; // The Fruit instance currently being controlled by the player var wallLeft, wallRight, gameFloor; // Boundary objects var dropPointY = GAME_CONSTANTS.DROP_POINT_Y; // Y position where fruits are aimed/dropped from var gameOver = false; // Game state flag var scoreText; // Text object for displaying score var isDragging = false; // If the player is currently dragging the active fruit // Charged Ball UI / Mechanic var chargedBallUI = null; // Instance of ChargedBallUI var chargeCounter = 0; // Counts drops towards charged ball release var readyToReleaseCharged = false; // Flag if the charged ball is ready // Merge tracking var mergeCounter = 0; // Counts merges, used for pineapple release var lastDroppedFruit = null; // Reference to the last fruit dropped by the player var lastDroppedHasMerged = false; // Flag if the last dropped fruit has successfully merged var spatialGrid = null; // Instance of SpatialGrid for collision optimization var lastScoreCheckForCoconut = 0; // Tracks score for spawning coconuts // --- Helper Functions --- function getFruitLevel(fruit) { if (!fruit || !fruit.type || !fruit.type.id) { // console.warn("getFruitLevel: Invalid fruit or fruit type. Defaulting to highest level to be safe."); return 10; // Default to a high level if type is unknown, makes it heavy. } var typeId = fruit.type.id.toUpperCase(); if (fruitLevels.hasOwnProperty(typeId)) { return fruitLevels[typeId]; } // console.warn("getFruitLevel: Unknown fruit type ID '" + typeId + "'. Defaulting."); return 10; } // Helper to get an "adjusted" radius for collision, potentially smaller for higher level fruits // This is a simplified version of the original hitbox reduction. function getAdjustedFruitRadius(fruit) { var baseRadius = fruit.width / 2; // Assuming width is representative for radius var level = getFruitLevel(fruit); // Reduce radius slightly for fruits above a certain level, e.g. level 4+ // var reduction = Math.max(0, level - 3) * GAME_CONSTANTS.FRUIT_HITBOX_REDUCTION_PER_LEVEL_DIFF; // For simplicity now, just use baseRadius or a fixed small reduction if needed. // The primary collision logic uses full AABB now, so complex radius adjustment is less critical. return baseRadius; } // --- Game Logic Functions --- function releasePineappleOnMerge() { if (mergeCounter >= GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE && !pineappleActive && pineapple) { pineappleActive = true; pineapple.isStatic = false; // Allow it to fall pineapple.immuneToGameOver = true; // Temporary immunity // --- FIX: Ensure pineapple and all its children are visible and alpha 1 --- pineapple.alpha = 1.0; pineapple.visible = true; if (pineapple.children && pineapple.children.length > 0) { for (var i = 0; i < pineapple.children.length; i++) { pineapple.children[i].visible = true; pineapple.children[i].alpha = 1.0; } } // Defensive: also ensure container itself is visible pineapple.visible = true; // Defensive: also ensure scale is correct (sometimes scale can be set to 0 by animation bugs) pineapple.scaleX = 1; pineapple.scaleY = 1; if (pineapple.children && pineapple.children.length > 0) { for (var i = 0; i < pineapple.children.length; i++) { pineapple.children[i].scaleX = 1; pineapple.children[i].scaleY = 1; } } applyDropPhysics(pineapple, 1.5); // Give it a gentle initial push fruits.push(pineapple); if (spatialGrid) { spatialGrid.insertObject(pineapple); } LK.setTimeout(function () { if (pineapple && fruits.indexOf(pineapple) !== -1) { // Check if it still exists and is in game pineapple.immuneToGameOver = false; pineapple.alpha = 1.0; // Ensure pineapple remains visible after immunity wears off pineapple.visible = true; if (pineapple.children && pineapple.children.length > 0) { for (var i = 0; i < pineapple.children.length; i++) { pineapple.children[i].visible = true; pineapple.children[i].alpha = 1.0; pineapple.children[i].scaleX = 1; pineapple.children[i].scaleY = 1; } } pineapple.scaleX = 1; pineapple.scaleY = 1; pineapple.visible = true; pineappleActive = false; // Reset flag ONLY after pineapple cycle is complete setupPineapple(); // Create a new static pineapple for the next cycle mergeCounter = 0; // Reset counter for next pineapple cycle } }, GAME_CONSTANTS.PINEAPPLE_IMMUNITY_MS); } } function setupBoundaries() { // Left Wall wallLeft = game.addChild(LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 })); wallLeft.x = 0; // Positioned at the left edge wallLeft.y = gameHeight / 2; wallLeft.alpha = 0; // Invisible, but physics active // Right Wall wallRight = game.addChild(LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 })); wallRight.x = gameWidth; // Positioned at the right edge wallRight.y = gameHeight / 2; wallRight.alpha = 0; // Floor gameFloor = game.addChild(LK.getAsset('floor', { anchorX: 0.5, anchorY: 0.5 })); gameFloor.x = gameWidth / 2; gameFloor.y = gameHeight; // Positioned at the bottom gameFloor.alpha = 0; // Game Over Line gameOverLine = game.addChild(new Line()); // Uses the Line class gameOverLine.x = gameWidth / 2; gameOverLine.y = GAME_CONSTANTS.GAME_OVER_LINE_Y; gameOverLine.scaleX = gameWidth / 100; // Scale to full screen width (Line asset is 100px wide) gameOverLine.scaleY = 0.2; // Make it thin gameOverLine.alpha = 1; // Visible } function createNextFruit() { var fruitProbability = Math.random(); // Determine next fruit type (e.g., mostly cherries, some grapes) var typeKey = fruitProbability < 0.65 ? 'CHERRY' : fruitProbability < 0.9 ? 'GRAPE' : 'APPLE'; // Added Apple for variety nextFruitType = FruitTypes[typeKey]; activeFruit = new Fruit(nextFruitType); // Position above drop point, centered based on last drop or default activeFruit.x = lastDroppedFruit && lastDroppedFruit.x ? lastDroppedFruit.x : gameWidth / 2; // The aiming Y position for dragging (where the fruit is shown before drop) activeFruit.y = dropPointY - GAME_CONSTANTS.DROP_START_Y_OFFSET + 300; activeFruit.isStatic = true; // Static until dropped game.addChild(activeFruit); if (trajectoryLine) { trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y + getTrajectoryLineYOffset(activeFruit)); // Start trajectory below the fruit } } function dropFruit() { if (gameOver || !activeFruit || !isClickable) { return; // Can't drop if game over, no active fruit, or click delay active } isClickable = false; LK.setTimeout(function () { isClickable = true; }, GAME_CONSTANTS.CLICK_DELAY_MS); activeFruit.isStatic = false; // Make it dynamic // Do NOT reset activeFruit.y here; preserve its current position as set by dragging applyDropPhysics(activeFruit, 2.0); // Apply initial physics forces (forceMultiplier can be tuned) fruits.push(activeFruit); if (spatialGrid) { spatialGrid.insertObject(activeFruit); } lastDroppedFruit = activeFruit; lastDroppedHasMerged = false; // Reset merge tracking for this new fruit // Increment charge counter for special ability chargeCounter++; updateChargedBallDisplay(); if (chargeCounter >= GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE && !readyToReleaseCharged) { // releaseChargedBalls(); // This was the old name, new one is setReadyState on UI chargedBallUI && chargedBallUI.setReadyState(true); // Signal UI it's ready readyToReleaseCharged = true; // Game logic flag } // Grant a temporary grace period for merging activeFruit.mergeGracePeriodActive = true; LK.setTimeout(function () { if (activeFruit && fruits.indexOf(activeFruit) !== -1) { // Check if fruit still exists activeFruit.mergeGracePeriodActive = false; } }, GAME_CONSTANTS.MERGE_GRACE_MS); if (trajectoryLine) { // Clear trajectory line after dropping trajectoryLine.clearDots(); } // Reset 'fromChargedRelease' flag for all existing fruits (if it was used) for (var i = 0; i < fruits.length; i++) { if (fruits[i] && fruits[i].fromChargedRelease) { fruits[i].fromChargedRelease = false; } } LK.getSound('drop').play(); // Handle the "charged ball" release (Pickle Rick orange) if (readyToReleaseCharged && chargeCounter >= GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE) { LK.getSound('pickleRick').play(); var orange = new Fruit(FruitTypes.ORANGE); // Spawn an orange // Determine spawn position for the orange var minX = wallLeft.x + wallLeft.width / 2 + orange.width / 2 + 20; var maxX = wallRight.x - wallRight.width / 2 - orange.width / 2 - 20; orange.x = minX + Math.random() * (maxX - minX); orange.y = -orange.height; // Start off-screen from top orange.isStatic = false; applyDropPhysics(orange, 2.5 + (Math.random() * 1.0 - 0.5)); // Give it a varied drop force orange.fromChargedRelease = true; // Mark it as special game.addChild(orange); fruits.push(orange); if (spatialGrid) { spatialGrid.insertObject(orange); } chargeCounter = 0; // Reset charge resetChargedBalls(); // Reset UI readyToReleaseCharged = false; // Reset flag } activeFruit = null; // Current fruit is dropped, clear activeFruit LK.setTimeout(function () { createNextFruit(); // Prepare the next one after 200ms delay }, 200); } function applyDropPhysics(fruit, forceMultiplier) { var fruitLevel = getFruitLevel(fruit); // Base downward force, slightly adjusted by level (heavier fruits fall a bit 'straighter') var levelAdjustedForce = forceMultiplier * (1 - fruitLevel * 0.03); // Small reduction for higher levels // Small random horizontal angle for variation var angleSpread = 15; // degrees var angle = (Math.random() * angleSpread - angleSpread / 2) * (Math.PI / 180); // radians fruit.vx = Math.sin(angle) * levelAdjustedForce * 0.5; // Horizontal component is less forceful fruit.vy = Math.abs(Math.cos(angle) * levelAdjustedForce); // Ensure initial vy is downwards or zero if (fruit.vy < 0.1) { fruit.vy = 0.1; } // Minimum downward push fruit.angularVelocity = Math.random() * 0.02 - 0.01; // Tiny random initial spin // Recalculate gravity based on its specific properties if needed, or ensure it's set fruit.gravity = GAME_CONSTANTS.BASE_GRAVITY * (1 + Math.pow(fruitLevel, GAME_CONSTANTS.GRAVITY_LEVEL_POWER_SCALE) * GAME_CONSTANTS.GRAVITY_LEVEL_MULTIPLIER_ADJUSTED); // Reset physics states fruit.isSleeping = false; fruit.isFullyStabilized = false; fruit._sleepCounter = 0; fruit.immuneToGameOver = true; // Temporary immunity LK.setTimeout(function () { if (fruit && fruits.indexOf(fruit) !== -1) { fruit.immuneToGameOver = false; } }, GAME_CONSTANTS.FRUIT_IMMUNITY_MS); } function updateScoreDisplay() { if (scoreText) { // Ensure scoreText is initialized scoreText.setText(LK.getScore()); } } function setupUI() { // Score Text scoreText = new Text2("0", { // Assuming Text2 is a valid class in LK or Upit size: 200, fill: 0x000000 // Black color }); scoreText.anchor.set(0.5, 0.5); // Anchor to center game.addChild(scoreText); // Add to midground container instead of GUI layer scoreText.y = gameHeight / 2 - 700; // 800 pixels above center (moved 300px higher) scoreText.x = gameWidth / 2; // Center horizontally // Charged Ball Display setupChargedBallDisplay(); } function setupChargedBallDisplay() { if (chargedBallUI) { // If exists, destroy before creating new chargedBallUI.destroy(); } chargedBallUI = new ChargedBallUI(); game.addChild(chargedBallUI); // Add to main game stage // chargedBallUI.initialize() is called within its constructor } function updateChargedBallDisplay() { if (chargedBallUI) { chargedBallUI.updateChargeDisplay(chargeCounter); } } // function releaseChargedBalls() { // This logic is now more integrated // readyToReleaseCharged = true; // if (chargedBallUI) { // chargedBallUI.setReadyState(true); // } // } function resetChargedBalls() { if (chargedBallUI) { chargedBallUI.reset(); } readyToReleaseCharged = false; // Also reset the game logic flag chargeCounter = 0; // Reset the counter itself updateChargedBallDisplay(); // Update UI to reflect reset } function checkFruitCollisions() { // Reset neighbor/surrounded counts for all fruits before collision checks for (var k = 0; k < fruits.length; k++) { var fruitInstance = fruits[k]; if (fruitInstance) { fruitInstance.neighborContacts = []; // Reset list of direct contacts // fruitInstance.surroundedFrames = 0; // This might be better managed by PhysicsComponent based on continuous contact } } outerLoop: for (var i = fruits.length - 1; i >= 0; i--) { var fruit1 = fruits[i]; if (!fruit1 || fruit1 === activeFruit || fruit1.merging || fruit1.isStatic || fruit1.isSleeping) { // Sleeping fruits don't initiate active collision response continue; } var candidates = spatialGrid.getPotentialCollisions(fruit1); for (var j = 0; j < candidates.length; j++) { var fruit2 = candidates[j]; // Basic checks to ensure fruit2 is valid for collision if (!fruit2 || fruit2 === activeFruit || fruit2.merging || fruit2.isStatic || fruit1 === fruit2) { continue; } // Ensure fruit2 is actually in the main fruits array (sanity check) if (fruits.indexOf(fruit2) === -1) { continue; } // --- Collision Detection (Ellipse-Ellipse for merging, AABB for physics) --- var f1HalfWidth = fruit1.width / 2; var f1HalfHeight = fruit1.height / 2; var f2HalfWidth = fruit2.width / 2; var f2HalfHeight = fruit2.height / 2; var dx = fruit2.x - fruit1.x; var dy = fruit2.y - fruit1.y; // Use ellipse-ellipse intersection for merging (touch = merge) var shouldMerge = false; if (fruit1.type.id === fruit2.type.id) { // Use the same ellipseEllipseIntersect as TrajectoryLine // Defensive: check if TrajectoryLine exists, else define a local function var ellipseEllipseIntersect = typeof TrajectoryLine !== "undefined" && TrajectoryLine.prototype && TrajectoryLine.prototype.ellipseEllipseIntersect ? TrajectoryLine.prototype.ellipseEllipseIntersect : function (x1, y1, w1, h1, x2, y2, w2, h2) { // If both ellipses are circles, use circle distance if (Math.abs(w1 - h1) < 1 && Math.abs(w2 - h2) < 1) { var r1 = w1 / 2; var r2 = w2 / 2; var dx = x1 - x2; var dy = y1 - y2; return dx * dx + dy * dy <= (r1 + r2) * (r1 + r2); } // For general ellipses, use a quick approximation: var dx = x1 - x2; var dy = y1 - y2; var rx = w1 / 2 + w2 / 2; var ry = h1 / 2 + h2 / 2; var norm = dx * dx / (rx * rx) + dy * dy / (ry * ry); return norm <= 1.0; }; shouldMerge = ellipseEllipseIntersect(fruit1.x, fruit1.y, fruit1.width, fruit1.height, fruit2.x, fruit2.y, fruit2.width, fruit2.height); } // Use AABB for physics collision response (as before) var collidingX = Math.abs(dx) < f1HalfWidth + f2HalfWidth; var collidingY = Math.abs(dy) < f1HalfHeight + f2HalfHeight; if (collidingX && collidingY) { // Wake up sleeping fruits if they are involved in a collision if (fruit1.isSleeping) { fruit1.wakeUp(); } if (fruit2.isSleeping) { fruit2.wakeUp(); } // Track neighbor contacts if (fruit1.neighborContacts.indexOf(fruit2.id) === -1) { fruit1.neighborContacts.push(fruit2.id); } if (fruit2.neighborContacts.indexOf(fruit1.id) === -1) { fruit2.neighborContacts.push(fruit1.id); } // --- Merge Check --- // Identical fruits should always merge when they touch - don't do physics collision if (fruit1.type.id === fruit2.type.id && shouldMerge) { fruit1.merge(fruit2); // Delegate to fruit's merge component continue outerLoop; // Critical: restart outer loop as fruit1 (and fruit2) might be gone } // --- Collision Response (Simplified Impulse-Based) --- var distance = Math.sqrt(dx * dx + dy * dy); if (distance === 0) { // Prevent division by zero if perfectly overlapped distance = 0.1; dx = 0.1; // Arbitrary small separation } var normalX = dx / distance; var normalY = dy / distance; // 1. Separation (Resolve Overlap) // Calculate overlap based on AABB - this is a simplification. // For circular, it's (r1+r2) - distance. Here, use an estimate. var overlapX = f1HalfWidth + f2HalfWidth - Math.abs(dx); var overlapY = f1HalfHeight + f2HalfHeight - Math.abs(dy); var overlap = Math.min(overlapX, overlapY) * GAME_CONSTANTS.FRUIT_COLLISION_SEPARATION_FACTOR; // Use smaller overlap dimension if (overlap > 0) { // Distribute separation based on "mass" (derived from level) var level1 = getFruitLevel(fruit1); var level2 = getFruitLevel(fruit2); // Reduce the mass power to make heavier fruits resist being pushed more (was 1.5, now 1.1) var mass1 = Math.pow(level1, 1.1) || 1; var mass2 = Math.pow(level2, 1.1) || 1; var totalMass = mass1 + mass2; // --- Emphasize vertical resolution --- // Compute a bias to favor vertical (downward/upward) separation // If fruit1 is below fruit2, bias the separation vector more vertical for fruit1 (and vice versa) var verticalBias = 0.33; // 0 = no bias, 1 = full vertical, 0.33 is a stronger nudge var sepNormalX = normalX; var sepNormalY = normalY; if (Math.abs(normalY) > 0.5) { // If the collision is already mostly vertical, keep as is sepNormalX = normalX; sepNormalY = normalY; } else { // If mostly horizontal, blend in more vertical if (fruit1.y > fruit2.y) { // fruit1 is below, bias its separation more upward sepNormalX = normalX * (1 - verticalBias); sepNormalY = normalY * (1 - verticalBias) - verticalBias; } else { // fruit1 is above, bias its separation more downward sepNormalX = normalX * (1 - verticalBias); sepNormalY = normalY * (1 - verticalBias) + verticalBias; } // Normalize the separation vector var sepLen = Math.sqrt(sepNormalX * sepNormalX + sepNormalY * sepNormalY); if (sepLen > 0) { sepNormalX /= sepLen; sepNormalY /= sepLen; } } // Reduce separation factor to prevent excessive push var separation1 = mass2 / totalMass * overlap * 0.7; var separation2 = mass1 / totalMass * overlap * 0.7; fruit1.x -= sepNormalX * separation1; fruit1.y -= sepNormalY * separation1; fruit2.x += sepNormalX * separation2; fruit2.y += sepNormalY * separation2; } // 2. Impulse (Exchange Momentum) var relVx = fruit2.vx - fruit1.vx; var relVy = fruit2.vy - fruit1.vy; var contactVelocity = relVx * normalX + relVy * normalY; if (contactVelocity < 0) { // If objects are moving towards each other var combinedElasticity = Math.min(fruit1.elasticity, fruit2.elasticity); // Or average, or max // Reduce impulse magnitude to prevent jerky movement and tunneling var impulseMagnitude = -(1 + combinedElasticity) * contactVelocity * 0.5; // Reduced from 0.7 to 0.5 // Cap maximum impulse magnitude to prevent extreme forces causing unnatural movement var maxImpulse = 3.0; // Maximum allowed impulse magnitude impulseMagnitude = Math.min(Math.abs(impulseMagnitude), maxImpulse) * (impulseMagnitude < 0 ? -1 : 1); // Distribute impulse based on mass var impulseRatio1 = mass2 / totalMass; var impulseRatio2 = mass1 / totalMass; // Calculate impulse vectors with distance-based scaling var distanceScale = Math.min(1.0, Math.max(0.2, distance / (fruit1.width + fruit2.width))); // Scale by relative distance fruit1.vx -= impulseMagnitude * normalX * impulseRatio1 * distanceScale; fruit1.vy -= impulseMagnitude * normalY * impulseRatio1 * distanceScale; fruit2.vx += impulseMagnitude * normalX * impulseRatio2 * distanceScale; fruit2.vy += impulseMagnitude * normalY * impulseRatio2 * distanceScale; // Wake up fruits if impulse is significant if (Math.abs(impulseMagnitude * impulseRatio1) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_LINEAR) { fruit1.wakeUp(); } if (Math.abs(impulseMagnitude * impulseRatio2) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_LINEAR) { fruit2.wakeUp(); } // 3. Friction (Tangential Impulse) - Simplified var tangentX = -normalY; var tangentY = normalX; var tangentVelocity = relVx * tangentX + relVy * tangentY; // Make friction coefficient stronger when normal force (contactVelocity) is higher var frictionBase = GAME_CONSTANTS.FRUIT_COLLISION_FRICTION_COEFFICIENT; var frictionBoost = Math.min(Math.abs(contactVelocity) * 0.12, 0.18); // Up to +0.18 for strong contacts var frictionImpulseMag = -tangentVelocity * (frictionBase + frictionBoost); fruit1.vx -= frictionImpulseMag * tangentX * impulseRatio1; fruit1.vy -= frictionImpulseMag * tangentY * impulseRatio1; fruit2.vx += frictionImpulseMag * tangentX * impulseRatio2; fruit2.vy += frictionImpulseMag * tangentY * impulseRatio2; // 4. Rotational Impulse (very simplified, can be made more complex if needed) // This makes fruits spin a little on glancing blows var angularImpulse = tangentVelocity * GAME_CONSTANTS.FRUIT_COLLISION_ROTATION_TRANSFER; // Distribute angular impulse (inverse of linear mass ratio for inertia approximation) // This is a rough approximation. True rotational inertia is more complex. var angImpRatio1 = impulseRatio2; // Fruit with less "linear push back" gets more spin var angImpRatio2 = impulseRatio1; fruit1.angularVelocity -= angularImpulse * angImpRatio1; fruit2.angularVelocity += angularImpulse * angImpRatio2; if (Math.abs(angularImpulse * angImpRatio1) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_ANGULAR) { fruit1.wakeUp(); } if (Math.abs(angularImpulse * angImpRatio2) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_ANGULAR) { fruit2.wakeUp(); } // Immediately apply significant angular damping after collision to both fruits fruit1.angularVelocity *= 0.85; fruit2.angularVelocity *= 0.85; // 5. Rotational Friction (Frictional force to reduce relative angular velocities) var angularDifference = fruit1.angularVelocity - fruit2.angularVelocity; // Make frictionStrength stronger when relative linear velocity is low (grinding, not impact) var relLinearVel = Math.sqrt(relVx * relVx + relVy * relVy); var frictionStrength = relLinearVel < 0.2 ? 0.08 : 0.03; // Stronger friction if nearly at rest fruit1.angularVelocity -= angularDifference * frictionStrength; fruit2.angularVelocity += angularDifference * frictionStrength; // Optionally, apply a little extra damping when in contact fruit1.angularVelocity *= 0.97; fruit2.angularVelocity *= 0.97; } } } } } 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 || fruit.immuneToGameOver) { continue; } // Calculate effective top Y of the fruit, considering rotation (simplified) var fruitHalfHeight = fruit.height / 2; var cosAngle = Math.abs(Math.cos(fruit.rotation)); var sinAngle = Math.abs(Math.sin(fruit.rotation)); // Using width for sin component of height projection var effectiveProjectedHeight = fruitHalfHeight * cosAngle + fruit.width / 2 * sinAngle; var fruitTopY = fruit.y - effectiveProjectedHeight; if (fruitTopY <= GAME_CONSTANTS.GAME_OVER_LINE_Y) { // A fruit is above or touching the game over line. // Now check if it's "stable" or "settled" there. // Stable means low velocity AND either fully stabilized or sleeping. var isMovingSlowly = Math.abs(fruit.vy) < 0.5 && Math.abs(fruit.vx) < 0.5; // Tune these thresholds var isConsideredSettled = isMovingSlowly && (fruit.isFullyStabilized || fruit.isSleeping); // If it's also somewhat still (e.g., low vy or stabilized state) if (isConsideredSettled || fruit.wallContactFrames > 0 && isMovingSlowly) { // Wall contact implies it might be stuck if (!fruit.gameOverTimer) { // Start timer if not already started fruit.gameOverTimer = Date.now(); // Optional: Visual feedback like blinking tween(fruit, { alpha: 0.5 }, { duration: 200, yoyo: true, repeat: 5, easing: tween.easeInOut }); } // Check if timer has exceeded countdown if (Date.now() - fruit.gameOverTimer >= GAME_CONSTANTS.GAME_OVER_COUNTDOWN_MS) { gameOver = true; LK.showGameOver(); // Assumes LK provides this return; // Exit loop and function } } else { // If fruit is above line but moving significantly, reset its timer if (fruit.gameOverTimer) { fruit.gameOverTimer = null; fruit.alpha = 1.0; // Reset visual feedback } } } else { // Fruit is below the line, reset its timer if it had one if (fruit.gameOverTimer) { fruit.gameOverTimer = null; fruit.alpha = 1.0; // Reset visual feedback } } } } function setupPineapple() { if (pineapple) { pineapple.destroy(); } // Remove old one if exists pineapple = new Fruit(FruitTypes.PINEAPPLE); pineapple.x = -pineapple.width + 350; // Start 200px further to the right than before pineapple.y = GAME_CONSTANTS.PINEAPPLE_START_Y; pineapple.isStatic = true; // Static until activated pineappleActive = false; // Not yet dropped // pineapplePushCount = 0; // Reset if this counter is used elsewhere game.addChild(pineapple); } function pushPineapple() { // Always animate the pineapple moving a bit to the right after every merge if (!pineappleActive && pineapple) { // Move pineapple a bit to the right for each merge, up to a max position var minX = -pineapple.width; // Off-screen left var maxX = gameWidth * GAME_CONSTANTS.PINEAPPLE_END_POS_FACTOR; // Target resting X // Each merge pushes pineapple a bit to the right var mergesNeeded = GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE; // Use Math.min to clamp mergeCounter to mergesNeeded var progress = Math.min(mergeCounter, mergesNeeded) / mergesNeeded; var newX = minX + progress * (maxX - minX); // Stop any existing tween to avoid conflicts tween.stop(pineapple, { x: true }); // Create a new tween with the updated position tween(pineapple, { x: newX }, { duration: GAME_CONSTANTS.PINEAPPLE_TWEEN_DURATION, easing: tween.bounceOut // Or a smoother ease like tween.easeOut }); } } function initGame() { LK.setScore(0); gameOver = false; // Clear existing fruits for (var i = fruits.length - 1; i >= 0; i--) { if (fruits[i]) { if (spatialGrid) { spatialGrid.removeObject(fruits[i]); } fruits[i].destroy(); } } fruits = []; // Reset UI and game state variables if (chargedBallUI) { chargedBallUI.destroy(); } // Destroy if exists chargedBallUI = null; // Ensure it's null before recreating chargeCounter = 0; readyToReleaseCharged = false; lastScoreCheckForCoconut = 0; lastDroppedFruit = null; lastDroppedHasMerged = false; mergeCounter = 0; isClickable = true; // Clear fire elements if (fireContainer) { for (var i = activeFireElements.length - 1; i >= 0; i--) { if (activeFireElements[i]) { activeFireElements[i].destroy(); } } fireContainer.destroy(); // Destroy the container itself } fireContainer = new Container(); // Create a new one game.addChildAt(fireContainer, 0); // Add to bottom of display stack activeFireElements = []; // Initialize Spatial Grid if (spatialGrid) { spatialGrid.clear(); } else { var avgFruitSize = 0; var fruitTypeCount = 0; for (var typeKey in FruitTypes) { avgFruitSize += FruitTypes[typeKey].size; fruitTypeCount++; } avgFruitSize = fruitTypeCount > 0 ? avgFruitSize / fruitTypeCount : 300; // Default if no types var cellSize = Math.ceil(avgFruitSize * GAME_CONSTANTS.SPATIAL_GRID_CELL_SIZE_FACTOR); spatialGrid = new SpatialGrid(cellSize); } // Destroy and recreate boundaries and UI elements if (wallLeft) { wallLeft.destroy(); } if (wallRight) { wallRight.destroy(); } if (gameFloor) { gameFloor.destroy(); } if (gameOverLine) { gameOverLine.destroy(); } if (pineapple) { pineapple.destroy(); } // Special pineapple if (trajectoryLine) { trajectoryLine.destroy(); } if (evolutionLine) { evolutionLine.destroy(); } if (scoreText) { // If scoreText was added to LK.gui.top LK.gui.top.removeChild(scoreText); // Remove from specific GUI layer scoreText.destroy(); // Then destroy the text object itself scoreText = null; // Nullify reference } LK.playMusic('bgmusic'); // Restart music setupBoundaries(); setupUI(); // This will create scoreText and chargedBallUI setupPineapple(); // Creates the initial static pineapple updateFireBackground(); // Initial fire setup trajectoryLine = game.addChild(new TrajectoryLine()); // trajectoryLine.createDots(); // Called by constructor or as needed by updateTrajectory evolutionLine = game.addChild(new EvolutionLine()); // evolutionLine.initialize(); // Called by its constructor updateScoreDisplay(); // Set score to 0 initially activeFruit = null; // No fruit being controlled yet createNextFruit(); // Prepare the first fruit resetChargedBalls(); // Ensure charge UI is reset } function spawnCoconut() { var coconut = new Fruit(FruitTypes.COCONUT); var minX = wallLeft.x + wallLeft.width / 2 + coconut.width / 2 + 30; // Buffer from wall var maxX = wallRight.x - wallRight.width / 2 - coconut.width / 2 - 30; coconut.x = minX + Math.random() * (maxX - minX); coconut.y = gameHeight + coconut.height; // Start below screen coconut.isStatic = true; // Initially static for tweening game.addChild(coconut); // Do not add to fruits array or spatial grid until it becomes dynamic coconut.immuneToGameOver = true; // Immune during spawn animation var targetY = gameHeight - gameFloor.height / 2 - coconut.height / 2 - 10; // Target just above floor tween(coconut, { y: targetY }, { duration: 1200, // Tween duration easing: tween.easeOut, // Smoother easing for arrival onFinish: function onFinish() { if (!coconut || coconut.parent === null) { return; } // Check if still exists coconut.isStatic = false; // Now it's dynamic fruits.push(coconut); // Add to main simulation arrays if (spatialGrid) { spatialGrid.insertObject(coconut); } coconut.vy = -1.5; // Small upward pop coconut.vx = (Math.random() * 2 - 1) * 1.0; // Small random horizontal push LK.setTimeout(function () { if (coconut && fruits.indexOf(coconut) !== -1) { coconut.immuneToGameOver = false; } }, 1000); // Immunity wears off } }); } // --- Player Input --- game.down = function (x, y) { if (activeFruit && !gameOver && isClickable) { // Check isClickable here too for initial press isDragging = true; game.move(x, y); // Process initial position } }; game.move = function (x, y) { if (isDragging && activeFruit && !gameOver) { var fruitRadius = activeFruit.width / 2; // Constrain X position within walls 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)); // Y position is fixed for aiming activeFruit.y = dropPointY - GAME_CONSTANTS.DROP_START_Y_OFFSET + 300; if (trajectoryLine) { trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y + getTrajectoryLineYOffset(activeFruit)); // Start trajectory below the fruit } } }; game.up = function () { if (isDragging && activeFruit && isClickable && !gameOver) { dropFruit(); } isDragging = false; // Reset dragging state regardless }; // --- Main Game Loop --- function updatePhysicsMain() { // Renamed from updatePhysics to avoid conflict with Fruit.updatePhysics // Rebuild spatial grid periodically (e.g., if fruits stop moving but grid needs refresh) // More robust would be to rebuild if many fruits are marked dirty or after many removals. if (spatialGrid && Date.now() - spatialGrid.lastRebuildTime > spatialGrid.rebuildInterval) { spatialGrid.rebuildGrid(fruits); } // Phase 1: Apply physics forces and update velocities/positions for each fruit for (var i = fruits.length - 1; i >= 0; i--) { var fruit = fruits[i]; if (!fruit || fruit.isStatic || fruit.merging) { continue; } fruit.updatePhysics(); // Call the fruit's own physics update method } // Phase 2: Check and resolve fruit-to-fruit collisions checkFruitCollisions(); // Global function for inter-fruit collisions // Phase 3: Check and resolve boundary collisions, then finalize positions and grid updates for (var i = fruits.length - 1; i >= 0; i--) { var fruit = fruits[i]; if (!fruit || fruit.isStatic || fruit.merging) { continue; } var walls = { left: wallLeft, right: wallRight }; fruit.checkBoundaries(walls, gameFloor); // Fruit's method for boundary checks // Final check for very small velocities if fruit is stabilized or on ground to help it fully stop if ((fruit.isFullyStabilized || fruit._boundaryContacts && fruit._boundaryContacts.floor) && !fruit.isSleeping) { if (Math.abs(fruit.vx) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD * 0.5) { fruit.vx = 0; } if (Math.abs(fruit.vy) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD * 0.5) { fruit.vy = 0; } if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.STABLE_ANGULAR_VELOCITY_THRESHOLD * 0.5) { fruit.angularVelocity = 0; } } // Update fruit's position in the spatial grid after all movements and collision resolutions if (spatialGrid) { spatialGrid.updateObject(fruit); } } // Phase 4: (Original) Environmental interactions and position adjustments // The "gap below" logic was very complex and might be better handled by more robust general physics. // For now, this is removed to simplify. If fruits get stuck, simpler nudging can be added. } game.update = function () { if (gameOver) { return; // Stop updates if game over } // Coconut spawning logic var currentScore = LK.getScore(); if (currentScore >= lastScoreCheckForCoconut + GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) { // Update score checkpoint based on actual score and interval, to prevent multiple spawns if score jumps high lastScoreCheckForCoconut = Math.floor(currentScore / GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) * GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL; spawnCoconut(); } // This else-if ensures that if the score doesn't trigger a spawn, but surpasses the last checkpoint, // the checkpoint is updated. This handles cases where score might increase by less than the interval. // However, the above line `lastScoreCheckForCoconut = Math.floor(...)` already handles this. // else if (currentScore > lastScoreCheckForCoconut) { // lastScoreCheckForCoconut = Math.floor(currentScore / GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) * GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL; // } updatePhysicsMain(); // Main physics and collision update loop checkGameOver(); // Check game over conditions // Update visual effects like fire updateFireBackground(); for (var i = 0; i < activeFireElements.length; i++) { if (activeFireElements[i]) { activeFireElements[i].update(); // Assuming FireElement has an update method } } }; // Initialize and start the game initGame(); // --- Fire Background Logic --- function updateFireBackground() { var uniqueFruitTypes = {}; for (var i = 0; i < fruits.length; i++) { var fruit = fruits[i]; // Consider only dynamic, non-merging fruits with valid types if (fruit && !fruit.isStatic && !fruit.merging && fruit.type && fruit.type.id) { uniqueFruitTypes[fruit.type.id] = true; } } var uniqueTypeCount = Object.keys(uniqueFruitTypes).length; 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); // Add new fire elements if needed while (activeFireElements.length < targetFireCount) { createFireElement(activeFireElements.length); // Pass current count as index for positioning } // Remove excess fire elements while (activeFireElements.length > targetFireCount) { var fireToRemove = activeFireElements.pop(); if (fireToRemove) { fireToRemove.destroy(); // Ensure proper cleanup of the fire element // fireContainer.removeChild(fireToRemove); // Destroy should handle this if it's a PIXI object } } } function createFireElement(index) { // Calculate position based on index to stack them or spread them out 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); // Assuming FireElement constructor sets up PIXI parts if (index % 2 === 1) { // Alternate facing direction for visual variety if (newFire.fireAsset) { newFire.fireAsset.scaleX = -1; } if (newFire.fireAsset2) { newFire.fireAsset2.scaleX = -1; } } fireContainer.addChildAt(newFire, 0); // Add to the bottom of the fire container for layering activeFireElements.push(newFire); } function removeFruitFromGame(fruit) { if (!fruit) { return; } // Wake up any sleeping neighbors that might have been resting on this fruit. // This is a simplified approach. More robust would be to check if they *were* supported. if (fruit.neighborContacts && fruit.neighborContacts.length > 0) { for (var i = 0; i < fruit.neighborContacts.length; i++) { var neighborId = fruit.neighborContacts[i]; for (var j = 0; j < fruits.length; j++) { if (fruits[j] && fruits[j].id === neighborId) { if (fruits[j].isSleeping || fruits[j].isFullyStabilized) { fruits[j].wakeUp(); // Use the fruit's own wakeUp method // Optionally give a very small nudge to ensure physics re-evaluates fruits[j].vy -= 0.05; // Tiny upward nudge fruits[j].vx += (Math.random() - 0.5) * 0.1; // Tiny sideways nudge } break; } } } } var index = fruits.indexOf(fruit); if (index !== -1) { fruits.splice(index, 1); } if (spatialGrid) { spatialGrid.removeObject(fruit); } // If the fruit being removed was the 'lastDroppedFruit', nullify the reference if (lastDroppedFruit === fruit) { lastDroppedFruit = null; } // If it was the active aiming fruit (shouldn't happen if physics are involved, but defensive) if (activeFruit === fruit) { activeFruit = null; // Potentially createNextFruit() here if game logic requires immediate replacement } fruit.destroy(); // Call the fruit's own destroy method (should handle PIXI cleanup) }
/****
* 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);
// wallContactFrames is now primarily for tracking if there *is* contact,
// rather than complex progressive friction which has been simplified.
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;
// Simplified effective dimension calculation - can be refined if necessary
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
};
// Reset contacts each frame before checking
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 isInContactWithBoundary = fruit._boundaryContacts.left || fruit._boundaryContacts.right || fruit._boundaryContacts.floor;
if (isInContactWithBoundary) {
self.wallContactFrames = Math.min(self.wallContactFrames + 1, GAME_CONSTANTS.MAX_WALL_CONTACT_FRAMES_FOR_FRICTION_EFFECT); // Cap frames for effect
// Apply a consistent friction/damping if in contact with wall/floor
// This is now handled more directly in PhysicsComponent based on contact flags
} else {
self.wallContactFrames = Math.max(0, self.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;
// Remove bounce: set vx to 0 and apply friction instead of reversing velocity
fruit.vx = 0;
if (Math.abs(incomingVx) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) {
LK.getSound('bounce').play();
}
// Remove angular impulse from wall collision, but keep a little damping for realism
fruit.angularVelocity *= GAME_CONSTANTS.WALL_ANGULAR_DAMPING;
fruit._boundaryContacts.left = true;
}
};
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;
// Remove bounce: set vx to 0 and apply friction instead of reversing velocity
fruit.vx = 0;
if (Math.abs(incomingVx) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) {
LK.getSound('bounce').play();
}
// Remove angular impulse from wall collision, but keep a little damping for realism
fruit.angularVelocity *= GAME_CONSTANTS.WALL_ANGULAR_DAMPING;
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 * GAME_CONSTANTS.FLOOR_BOUNCE_DAMPING;
if (Math.abs(incomingVy) > GAME_CONSTANTS.BOUNCE_SOUND_VELOCITY_THRESHOLD) {
LK.getSound('bounce').play();
}
fruit._boundaryContacts.floor = true;
// Apply friction to linear velocity when on floor (more direct than before)
fruit.vx *= GAME_CONSTANTS.GROUND_LINEAR_FRICTION * 0.9; // Increase friction
// Extra: If vx is very small, apply stronger friction to help it stop
if (Math.abs(fruit.vx) < 0.05) {
fruit.vx *= 0.7; // Stronger friction
}
// Simpler angular velocity effect from floor contact (rolling)
if (Math.abs(fruit.vx) > GAME_CONSTANTS.MIN_VX_FOR_ROLLING) {
// A small portion of vx is converted to angularVelocity to simulate rolling
var targetAngular = fruit.vx * GAME_CONSTANTS.GROUND_ROLLING_FACTOR;
// Blend towards target rolling angular velocity
fruit.angularVelocity = fruit.angularVelocity * 0.7 + targetAngular * 0.3;
}
// Increase ground angular damping for more rapid spin decay
fruit.angularVelocity *= 0.80; // Stronger than before
// Extra: If angular velocity is very small, apply stronger friction to help it stop
if (Math.abs(fruit.angularVelocity) < 0.01) {
fruit.angularVelocity *= 0.5; // Even stronger friction
}
// Simplified resting condition
if (Math.abs(fruit.vy) < GAME_CONSTANTS.RESTING_VELOCITY_THRESHOLD) {
fruit.vy = 0;
}
if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.ANGULAR_RESTING_THRESHOLD) {
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;
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;
}
};
// Call initialize in the constructor pattern
self.initialize();
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); // Call parent's destroy
};
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;
// Components are instantiated here
var physics = new PhysicsComponent(); // Manages physics updates, stabilization, sleeping
var collision = new CollisionComponent(); // Manages boundary collisions
var mergeHandler = new MergeComponent(); // Manages merging logic
var behaviorSystem = new FruitBehavior(); // Manages type-specific behaviors
// Physics properties are on the fruit itself, managed by PhysicsComponent
self.vx = 0;
self.vy = 0;
self.rotation = 0;
self.angularVelocity = 0;
var currentLevel = getFruitLevel(self); // Helper to get fruit level
self.gravity = GAME_CONSTANTS.BASE_GRAVITY * (1 + Math.pow(currentLevel, GAME_CONSTANTS.GRAVITY_LEVEL_POWER_SCALE) * GAME_CONSTANTS.GRAVITY_LEVEL_MULTIPLIER_ADJUSTED);
// Elasticity: Higher level fruits are slightly less bouncy
self.elasticity = Math.max(GAME_CONSTANTS.MIN_ELASTICITY, GAME_CONSTANTS.BASE_ELASTICITY - currentLevel * GAME_CONSTANTS.ELASTICITY_DECREASE_PER_LEVEL);
// State flags
self.isStatic = false; // If true, physics is not applied (e.g., next fruit display)
self.isSleeping = false; // If true, physics updates are minimal (already resting)
self.isFullyStabilized = false; // Intermediate state for coming to rest
self._sleepCounter = 0; // Counter for consecutive frames with low energy to trigger sleeping
self.wallContactFrames = 0; // Now simpler, tracked by CollisionComponent but can be read by Fruit if needed
// Merge related properties
self.merging = false; // True if this fruit is currently in a merge animation
self.mergeGracePeriodActive = false; // Short period after drop where merges are prioritized
self.fromChargedRelease = false; // If this fruit spawned from the special charge release
// Game logic flags
self.safetyPeriod = false; // Used in game over logic (original logic retained)
self.immuneToGameOver = false; // If true, this fruit cannot trigger game over
// Behavior (e.g., special merge effects)
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.warn("Fruit: Type not available or missing required properties for fruit ID: " + (type ? type.id : 'unknown'));
}
// Public methods to interact with components
self.updatePhysics = function () {
// Delegate to the physics component, passing self (the fruit instance)
physics.apply(self);
};
self.checkBoundaries = function (walls, floor) {
// Delegate to the collision component
collision.checkBoundaryCollisions(self, walls, floor);
self.wallContactFrames = collision.wallContactFrames; // Update fruit's copy if needed elsewhere
};
self.merge = function (otherFruit) {
// Delegate to the merge handler
mergeHandler.beginMerge(self, otherFruit);
};
// Method to wake up a fruit if it's sleeping or stabilized
self.wakeUp = function () {
if (self.isSleeping || self.isFullyStabilized) {
self.isSleeping = false;
self.isFullyStabilized = false;
self._sleepCounter = 0;
// Optional: apply a tiny impulse if needed to ensure it starts moving
// self.vy -= 0.1;
}
};
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(); // Assumes this global function exists
removeFruitFromGame(f1); // Assumes this global function exists
removeFruitFromGame(f2); // Assumes this global function exists
return null; // No new fruit created from Durian merge
}
}
};
self.getMergeHandler = function (fruitTypeId) {
if (!fruitTypeId) {
console.warn("FruitBehavior: fruitTypeId is null or undefined in getMergeHandler. Defaulting to CHERRY.");
return self.behaviors.CHERRY;
}
var upperTypeId = fruitTypeId.toUpperCase();
return self.behaviors[upperTypeId] || self.behaviors.CHERRY;
};
self.standardMerge = function (fruit1, fruit2, posX, posY) {
var nextFruitKey = fruit1.type.next;
if (!nextFruitKey) {
console.error("FruitBehavior: 'next' type is undefined for fruit type: " + fruit1.type.id);
return null; // Cannot create next level fruit
}
var nextType = FruitTypes[nextFruitKey.toUpperCase()];
if (!nextType) {
console.error("FruitBehavior: Next fruit type '" + nextFruitKey + "' not found in FruitTypes.");
return null;
}
var newFruit = self.createNextLevelFruit(fruit1, nextType, posX, posY);
return newFruit;
};
self.createNextLevelFruit = function (sourceFruit, nextType, posX, posY) {
var newFruit = new Fruit(nextType); // Fruit class constructor
newFruit.x = posX;
newFruit.y = posY;
// Initial scale for merge animation
newFruit.scaleX = 0.5;
newFruit.scaleY = 0.5;
game.addChild(newFruit); // Assumes 'game' is a global Application or Container
fruits.push(newFruit); // Assumes 'fruits' is a global array
if (spatialGrid) {
// Assumes 'spatialGrid' is a global instance
spatialGrid.insertObject(newFruit);
}
LK.setScore(LK.getScore() + nextType.points);
updateScoreDisplay(); // Assumes this global function exists
tween(newFruit, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
// Standard merge pop-in duration
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', {
// Re-using floor asset for the line
anchorX: 0.5,
anchorY: 0.5
});
lineGraphics.tint = 0xff0000; // Red color for game over line
lineGraphics.height = 20; // Make it visually a line
// Width will be scaled in setupBoundaries
return self;
});
var MergeComponent = Container.expand(function () {
var self = Container.call(this);
self.merging = false; // This property seems to belong to the Fruit itself, not the component instance.
// Each fruit will have its own 'merging' flag.
self.fruitBehavior = new FruitBehavior(); // To get specific merge behaviors
self.beginMerge = function (fruit1, fruit2) {
// Prevent merging if either fruit is already involved in a merge
if (fruit1.merging || fruit2.merging) {
return;
}
// Prevent merging if fruit types are different (should be handled by collision check before calling this)
if (fruit1.type.id !== fruit2.type.id) {
console.warn("MergeComponent: Attempted to merge fruits of different types. This should be checked earlier.");
return;
}
fruit1.merging = true;
fruit2.merging = true;
var midX = (fruit1.x + fruit2.x) / 2;
var midY = (fruit1.y + fruit2.y) / 2;
self.animateMerge(fruit1, fruit2, midX, midY);
};
self.animateMerge = function (fruit1, fruit2, midX, midY) {
// Animate fruit1 shrinking and fading
tween(fruit1, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
// Optional: Move towards merge point if desired, though often they are already close
// x: midX,
// y: midY
}, {
duration: GAME_CONSTANTS.MERGE_ANIMATION_DURATION,
// Use a constant
easing: tween.easeOut
});
// Animate fruit2 shrinking and fading, then complete the merge
tween(fruit2, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
// x: midX,
// y: midY
}, {
duration: GAME_CONSTANTS.MERGE_ANIMATION_DURATION,
easing: tween.easeOut,
onFinish: function onFinish() {
self.completeMerge(fruit1, fruit2, midX, midY);
}
});
};
self.completeMerge = function (fruit1, fruit2, midX, midY) {
LK.getSound('merge').play();
self.trackMergeAnalytics(fruit1, fruit2); // For game analytics or special logic
var behaviorHandler = self.fruitBehavior.getMergeHandler(fruit1.type.id);
var newFruit = null;
if (behaviorHandler && behaviorHandler.onMerge) {
newFruit = behaviorHandler.onMerge(fruit1, fruit2, midX, midY);
} else {
// Fallback to standard merge if no specific behavior or handler is missing
console.warn("MergeComponent: No specific onMerge behavior for " + fruit1.type.id + ", or handler missing. Attempting standard merge.");
var nextFruitKey = fruit1.type.next;
if (nextFruitKey) {
var nextType = FruitTypes[nextFruitKey.toUpperCase()];
if (nextType) {
newFruit = self.fruitBehavior.createNextLevelFruit(fruit1, nextType, midX, midY);
} else {
console.error("MergeComponent: Fallback standard merge failed, next fruit type '" + nextFruitKey + "' not found.");
}
} else {
console.error("MergeComponent: Fallback standard merge failed, 'next' type is undefined for " + fruit1.type.id);
}
}
// Centralized authoritative point for incrementing mergeCounter and calling pushPineapple
mergeCounter++;
pushPineapple();
// Check for pineapple release after merge is complete
releasePineappleOnMerge();
// Clean up old fruits. This should happen *after* the new fruit is potentially created and added.
// Durian behavior handles its own removal.
if (fruit1.type.id.toUpperCase() !== 'DURIAN') {
removeFruitFromGame(fruit1); // Global removal function
}
// Fruit2 is always removed unless it was part of a special non-creating merge (like Durian's partner)
// The Durian logic already removes both.
if (fruit2.type.id.toUpperCase() !== 'DURIAN' || fruit1.type.id.toUpperCase() !== 'DURIAN') {
// If neither was a Durian, or if fruit1 was Durian (so fruit2 is its partner to be removed)
removeFruitFromGame(fruit2);
}
};
// This function seems to be for tracking specific game states related to merges
self.trackMergeAnalytics = function (fruit1, fruit2) {
// Original logic for lastDroppedHasMerged - ensure lastDroppedFruit is a global variable
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);
// Note: Properties like vx, vy, gravity, etc., are on the Fruit object itself.
// This component's methods will modify those properties on the passed 'fruit' instance.
self.apply = function (fruit) {
if (fruit.isStatic || fruit.merging) {
return; // No physics for static or merging fruits
}
// If fruit is sleeping, only perform minimal checks or wake it up if necessary.
if (fruit.isSleeping) {
// A sleeping fruit should not move. Velocities should already be zero.
fruit.vx = 0;
fruit.vy = 0;
fruit.angularVelocity = 0;
// Potentially check for significant external forces that might wake it up (handled in collision response)
return;
}
var prevVx = fruit.vx;
var prevVy = fruit.vy;
// 1. Apply Gravity
// fruit.gravity is pre-calculated on fruit creation based on its level
fruit.vy += fruit.gravity * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT; // Assuming a fixed timestep or adjusting for it
// 2. Apply Air Friction / Damping (if not in contact with ground)
if (!fruit._boundaryContacts || !fruit._boundaryContacts.floor) {
// Check if fruit is resting on another fruit (not in air, not on floor/walls)
var restingOnOtherFruit = false;
if (fruit._boundaryContacts && !fruit._boundaryContacts.floor && !fruit._boundaryContacts.left && !fruit._boundaryContacts.right) {
// If the fruit has neighborContacts, and at least one is directly below, consider it "resting on another fruit"
if (fruit.neighborContacts && fruit.neighborContacts.length > 0) {
for (var i = 0; i < fruits.length; i++) {
var neighbor = fruits[i];
if (neighbor && neighbor.id && fruit.neighborContacts.indexOf(neighbor.id) !== -1) {
// Check if neighbor is directly below (simple Y check, can be improved)
if (neighbor.y > fruit.y && Math.abs(neighbor.x - fruit.x) < (fruit.width + neighbor.width) / 2) {
restingOnOtherFruit = true;
break;
}
}
}
}
}
fruit.vx *= GAME_CONSTANTS.AIR_FRICTION_MULTIPLIER;
fruit.vy *= GAME_CONSTANTS.AIR_FRICTION_MULTIPLIER; // Air friction also affects vertical, less than gravity though
fruit.angularVelocity *= GAME_CONSTANTS.AIR_ANGULAR_DAMPING;
// Apply a small, constant angular damping every frame to gently slow down free-spinning in the air
fruit.angularVelocity *= 0.995;
// Extra angular friction in air to help stop spinning
if (Math.abs(fruit.angularVelocity) < 0.01) {
fruit.angularVelocity *= 0.90; // Increased friction
}
// Extra linear friction in air to help stop drifting
if (Math.abs(fruit.vx) < 0.05) {
fruit.vx *= 0.90; // Increased friction
}
if (Math.abs(fruit.vy) < 0.05) {
fruit.vy *= 0.90; // Increased friction
}
// If resting on another fruit, apply extra friction to help it come to rest
if (restingOnOtherFruit) {
fruit.vx *= 0.85; // Stronger friction
fruit.vy *= 0.85;
fruit.angularVelocity *= 0.85;
// If velocities are very small, damp even more
if (Math.abs(fruit.vx) < 0.02) {
fruit.vx *= 0.7;
}
if (Math.abs(fruit.vy) < 0.02) {
fruit.vy *= 0.7;
}
if (Math.abs(fruit.angularVelocity) < 0.005) {
fruit.angularVelocity *= 0.7;
}
}
} else {
// If on ground, specific ground friction is handled in CollisionComponent.checkFloorCollision
// and here for angular damping.
// Increase general ground contact angular damping
fruit.angularVelocity *= 0.88; // Stronger damping than before
// If fruit is on a surface (floor or wall) and linear velocity is low, apply even stronger angular damping
var onSurface = fruit._boundaryContacts && (fruit._boundaryContacts.floor || fruit._boundaryContacts.left || fruit._boundaryContacts.right);
if (onSurface && Math.abs(fruit.vx) < 0.08 && Math.abs(fruit.vy) < 0.08) {
fruit.angularVelocity *= 0.7; // Very strong damping when nearly at rest on a surface
}
// Extra angular friction on ground to help stop spinning
if (Math.abs(fruit.angularVelocity) < 0.01) {
fruit.angularVelocity *= 0.75; // Even more friction
}
// Extra linear friction on ground to help stop sliding
if (Math.abs(fruit.vx) < 0.05) {
fruit.vx *= 0.80; // Increased friction
}
if (Math.abs(fruit.vy) < 0.05) {
fruit.vy *= 0.80; // Increased friction
}
}
// 3. Update Rotation
fruit.rotation += fruit.angularVelocity * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT;
self.handleRotationLimits(fruit); // Cap angular velocity and handle near-zero stopping
// 4. Limit Velocities (Max Speeds)
var maxLinearSpeed = GAME_CONSTANTS.MAX_LINEAR_VELOCITY_BASE - getFruitLevel(fruit) * GAME_CONSTANTS.MAX_LINEAR_VELOCITY_REDUCTION_PER_LEVEL;
maxLinearSpeed = Math.max(GAME_CONSTANTS.MIN_MAX_LINEAR_VELOCITY, maxLinearSpeed);
var currentSpeed = Math.sqrt(fruit.vx * fruit.vx + fruit.vy * fruit.vy);
if (currentSpeed > maxLinearSpeed) {
var ratio = maxLinearSpeed / currentSpeed;
fruit.vx *= ratio;
fruit.vy *= ratio;
}
// 5. Update Position
fruit.x += fruit.vx * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT;
fruit.y += fruit.vy * GAME_CONSTANTS.PHYSICS_TIMESTEP_ADJUSTMENT;
// 6. Stabilization and Sleep Logic (Simplified)
self.checkStabilization(fruit, prevVx, prevVy);
// Ensure mid-air fruits are not marked as sleeping or stabilized
if (!fruit._boundaryContacts || !fruit._boundaryContacts.floor && !fruit._boundaryContacts.left && !fruit._boundaryContacts.right) {
fruit.isFullyStabilized = false;
fruit.isSleeping = false;
fruit._sleepCounter = 0;
}
// If the fruit is now fully stabilized (but not yet sleeping), force velocities to zero.
if (fruit.isFullyStabilized && !fruit.isSleeping) {
if (Math.abs(fruit.vx) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD && Math.abs(fruit.vy) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD && Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.STABLE_ANGULAR_VELOCITY_THRESHOLD) {
// This is a soft "almost stopped" state, let's push it to fully stopped
fruit.vx = 0;
fruit.vy = 0;
fruit.angularVelocity = 0;
}
// Additional: If velocities are extremely small, always zero them out
if (Math.abs(fruit.vx) < 0.005) {
fruit.vx = 0;
}
if (Math.abs(fruit.vy) < 0.005) {
fruit.vy = 0;
}
if (Math.abs(fruit.angularVelocity) < 0.0005) {
fruit.angularVelocity = 0;
}
}
// Ensure minimum fall speed if in air and moving downwards very slowly (prevents floating)
if (fruit.vy > 0 && fruit.vy < GAME_CONSTANTS.MIN_FALL_SPEED_IF_SLOWING && (!fruit._boundaryContacts || !fruit._boundaryContacts.floor)) {
fruit.vy = GAME_CONSTANTS.MIN_FALL_SPEED_IF_SLOWING;
}
};
self.handleRotationLimits = function (fruit) {
// Cap angular velocity
var maxAngVel = GAME_CONSTANTS.MAX_ANGULAR_VELOCITY_BASE - getFruitLevel(fruit) * GAME_CONSTANTS.MAX_ANGULAR_VELOCITY_REDUCTION_PER_LEVEL;
maxAngVel = Math.max(GAME_CONSTANTS.MIN_MAX_ANGULAR_VELOCITY, maxAngVel);
fruit.angularVelocity = Math.max(-maxAngVel, Math.min(maxAngVel, fruit.angularVelocity));
// If angular velocity is very low, and fruit is somewhat stable, stop rotation
if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.ANGULAR_STOP_THRESHOLD_PHYSICS) {
// Additional check: if linear velocity is also very low, it's more likely to stop rotating
var linSpeedSq = fruit.vx * fruit.vx + fruit.vy * fruit.vy;
if (linSpeedSq < GAME_CONSTANTS.LINEAR_SPEED_SQ_FOR_ANGULAR_STOP) {
fruit.angularVelocity = 0;
fruit.rotationRestCounter = (fruit.rotationRestCounter || 0) + 1;
} else {
fruit.rotationRestCounter = 0;
}
// Extra: Clamp to zero if extremely small
if (Math.abs(fruit.angularVelocity) < 0.0002) {
fruit.angularVelocity = 0;
}
} else {
fruit.rotationRestCounter = 0;
}
};
self.checkStabilization = function (fruit, prevVx, prevVy) {
// Calculate change in velocity (kinetic energy proxy)
var deltaVSq = (fruit.vx - prevVx) * (fruit.vx - prevVx) + (fruit.vy - prevVy) * (fruit.vy - prevVy);
var currentSpeedSq = fruit.vx * fruit.vx + fruit.vy * fruit.vy;
var currentAngularSpeed = Math.abs(fruit.angularVelocity);
// Conditions for considering a fruit "active" (not stabilizing)
// Lowered thresholds for more decisive stabilization
var isActive = currentSpeedSq > GAME_CONSTANTS.ACTIVE_LINEAR_SPEED_SQ_THRESHOLD * 0.7 || currentAngularSpeed > GAME_CONSTANTS.ACTIVE_ANGULAR_SPEED_THRESHOLD * 0.7 || deltaVSq > GAME_CONSTANTS.ACTIVE_DELTA_V_SQ_THRESHOLD * 0.7;
// Fruit is considered potentially stabilizing if it's in contact with something (floor or walls)
var onSurface = fruit._boundaryContacts && (fruit._boundaryContacts.floor || fruit._boundaryContacts.left || fruit._boundaryContacts.right);
if (!isActive && onSurface) {
fruit._sleepCounter = (fruit._sleepCounter || 0) + 1;
} else {
// If it becomes active or is in mid-air, reset counter and wake it up
fruit._sleepCounter = 0;
fruit.isSleeping = false;
fruit.isFullyStabilized = false; // If active, it's not stabilized
}
if (fruit._sleepCounter >= GAME_CONSTANTS.SLEEP_DELAY_FRAMES_SIMPLIFIED + 8) {
// If it has been inactive for enough frames (slightly longer to allow for jostle)
if (!fruit.isSleeping) {
// Only set to sleeping if not already
fruit.isSleeping = true;
fruit.isFullyStabilized = true; // Sleeping implies fully stabilized
fruit.vx = 0;
fruit.vy = 0;
fruit.angularVelocity = 0;
}
} else if (fruit._sleepCounter >= GAME_CONSTANTS.STABILIZED_DELAY_FRAMES) {
// If inactive for a shorter period, it's "fully stabilized" but not yet sleeping
// This state can be used for game logic like game over checks
if (!fruit.isFullyStabilized && !fruit.isSleeping) {
fruit.isFullyStabilized = true;
}
// More aggressively zero out velocities if below very small thresholds
if (Math.abs(fruit.vx) < 0.003) {
fruit.vx = 0;
}
if (Math.abs(fruit.vy) < 0.003) {
fruit.vy = 0;
}
if (Math.abs(fruit.angularVelocity) < 0.0002) {
fruit.angularVelocity = 0;
}
} else {
// If not meeting sleep or stabilized counts, it's not in these states
fruit.isSleeping = false;
fruit.isFullyStabilized = false;
}
};
return self;
});
var SpatialGrid = Container.expand(function (cellSize) {
var self = Container.call(this);
self.cellSize = cellSize || 200; // Default cell size
self.grid = {}; // Stores cellKey: [object1, object2, ...]
self.lastRebuildTime = Date.now();
self.rebuildInterval = GAME_CONSTANTS.SPATIAL_GRID_REBUILD_INTERVAL_MS; // How often to rebuild if objects don't move
self.insertObject = function (obj) {
if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number' || obj.merging || obj.isStatic) {
return; // Skip invalid, merging, or static objects
}
obj._currentCells = obj._currentCells || []; // Ensure property exists
var cells = self.getCellsForObject(obj);
obj._currentCells = cells.slice(); // Store the cells the object is currently in
for (var i = 0; i < cells.length; i++) {
var cellKey = cells[i];
if (!self.grid[cellKey]) {
self.grid[cellKey] = [];
}
// Avoid duplicates if somehow inserted twice
if (self.grid[cellKey].indexOf(obj) === -1) {
self.grid[cellKey].push(obj);
}
}
};
self.removeObject = function (obj) {
if (!obj || !obj._currentCells) {
// Check if obj is valid and has _currentCells
return;
}
var cells = obj._currentCells; // Use stored cells for removal efficiency
for (var i = 0; i < cells.length; i++) {
var cellKey = cells[i];
if (self.grid[cellKey]) {
var cellIndex = self.grid[cellKey].indexOf(obj);
if (cellIndex !== -1) {
self.grid[cellKey].splice(cellIndex, 1);
}
// Clean up empty cell arrays
if (self.grid[cellKey].length === 0) {
delete self.grid[cellKey];
}
}
}
obj._currentCells = []; // Clear stored cells
};
// Calculates which grid cells an object overlaps with
self.getCellsForObject = function (obj) {
if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number') {
return []; // Return empty array for invalid input
}
var cells = [];
// Using object's center and dimensions to find cell range
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); // Unique key for each cell
}
}
return cells;
};
self.updateObject = function (obj) {
if (!obj || typeof obj.x !== 'number' || typeof obj.y !== 'number' || typeof obj.width !== 'number' || typeof obj.height !== 'number' || obj.merging || obj.isStatic) {
// Don't update static or merging objects in the grid
return;
}
var newCells = self.getCellsForObject(obj);
var oldCells = obj._currentCells || [];
// Efficiently check if cells have changed
var cellsChanged = false;
if (oldCells.length !== newCells.length) {
cellsChanged = true;
} else {
// Check if all new cells are in oldCells (and vice-versa implied by same length)
for (var i = 0; i < newCells.length; i++) {
if (oldCells.indexOf(newCells[i]) === -1) {
cellsChanged = true;
break;
}
}
}
if (cellsChanged) {
self.removeObject(obj); // Remove from old cells
self.insertObject(obj); // Insert into new cells
}
};
// Gets potential collision candidates for an object
self.getPotentialCollisions = function (obj) {
var candidates = [];
if (!obj || typeof obj.x !== 'number') {
return candidates;
} // Basic check for valid object
var cells = self.getCellsForObject(obj); // Determine cells the object is in
var addedObjects = {}; // To prevent duplicate candidates if an object is in multiple shared cells
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];
// Ensure otherObj is valid, not the same as obj, and not already added
if (otherObj && otherObj !== obj && !addedObjects[otherObj.id]) {
if (otherObj.merging || otherObj.isStatic) {
continue;
} // Skip merging or static candidates
candidates.push(otherObj);
addedObjects[otherObj.id] = true; // Mark as added
}
}
}
}
return candidates;
};
self.clear = function () {
self.grid = {};
self.lastRebuildTime = Date.now();
};
// Rebuilds the entire grid. Useful if many objects become invalid or after a reset.
self.rebuildGrid = function (allObjects) {
self.grid = {}; // Clear current grid
self.lastRebuildTime = Date.now();
if (Array.isArray(allObjects)) {
for (var i = 0; i < allObjects.length; i++) {
// Only insert valid, non-merging, non-static objects
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(30); // Reduced default pool size, can grow
self.activeDots = []; // DisplayObjects currently on stage
// self.dots renamed to activeDots for clarity
self.dotSpacing = 25; // Spacing between dots
self.dotSize = 15; // Visual size (not used in current createObject)
// Dynamically calculate maxDots so the trajectory can reach from drop point to floor
// This is recalculated in case drop point or floor changes
function calculateMaxDots() {
var dropActualY = GAME_CONSTANTS.DROP_POINT_Y + GAME_CONSTANTS.DROP_START_Y_OFFSET;
var distanceToCover = GAME_CONSTANTS.GAME_HEIGHT - dropActualY;
var requiredDots = Math.ceil(distanceToCover / self.dotSpacing) + 5; // +5 for a little buffer
if (requiredDots <= 0) {
requiredDots = 60;
} // Fallback if calculation is off
return requiredDots;
}
self.maxDots = calculateMaxDots();
self.createDots = function () {
// Renamed from initialize for clarity
self.clearDots(); // Clear existing before creating new pool (if any)
// DotPool's initialize is called by its constructor if initialSize is given
};
self.clearDots = function () {
while (self.activeDots.length > 0) {
var dot = self.activeDots.pop();
if (dot) {
self.removeChild(dot); // Remove from PIXI stage
self.dotPool.release(dot); // Return to pool
}
}
};
self.updateTrajectory = function (startX, startY) {
if (!activeFruit) {
self.clearDots();
return;
}
self.clearDots();
// Start the trajectory line at the correct offset below the fruit's bottom
var currentY = startY;
var dotCount = 0;
var hitDetected = false;
var currentFruitRadius = activeFruit.width / 2;
var COLLISION_CHECK_INTERVAL = 1; // Check every dot for best accuracy
// Precompute floor collision Y for this fruit
var floorCollisionY = gameFloor.y - gameFloor.height / 2 - currentFruitRadius;
// We'll keep track of the first intersection (floor or fruit)
var firstHitY = null;
var firstHitType = null; // "floor" or "fruit"
var firstHitFruit = null;
// Always use a fixed spacing between dots, regardless of how far the line goes
var maxDistance = floorCollisionY - startY;
var maxDotsByDistance = Math.ceil(Math.abs(maxDistance) / self.dotSpacing) + 5;
var maxDots = Math.min(self.maxDots, maxDotsByDistance);
for (dotCount = 0; dotCount < maxDots && !hitDetected; dotCount++) {
var dot = self.dotPool.get();
if (!dot) {
continue;
}
self.addChild(dot);
self.activeDots.push(dot);
dot.x = startX;
dot.y = startY + dotCount * self.dotSpacing;
dot.visible = true;
dot.alpha = 1.0 - dotCount / self.maxDots * 0.7;
dot.scale.set(1.0 - dotCount / self.maxDots * 0.5);
var thisY = dot.y;
// Check for floor collision
if (thisY > floorCollisionY && !hitDetected) {
firstHitY = floorCollisionY;
firstHitType = "floor";
hitDetected = true;
break;
}
// Check for fruit collisions
if (dotCount % COLLISION_CHECK_INTERVAL === 0 && !hitDetected) {
var trajectoryCheckObject = {
x: startX,
y: thisY,
width: activeFruit.width,
height: activeFruit.height,
id: 'trajectory_check_point'
};
var potentialHits = spatialGrid.getPotentialCollisions(trajectoryCheckObject);
for (var j = 0; j < potentialHits.length; j++) {
var fruit = potentialHits[j];
if (fruit && fruit !== activeFruit && !fruit.merging && !fruit.isStatic && fruit.width && fruit.height && fruit._boundaryContacts && fruit._boundaryContacts.floor // Only landed fruits
) {
if (self.ellipseEllipseIntersect(startX, thisY, activeFruit.width, activeFruit.height, fruit.x, fruit.y, fruit.width, fruit.height)) {
// Found a hit with a fruit
firstHitY = thisY;
firstHitType = "fruit";
firstHitFruit = fruit;
hitDetected = true;
break;
}
}
}
}
if (hitDetected) {
break;
}
}
// If we hit something, adjust the last dot to be exactly at the intersection
if (hitDetected && self.activeDots.length > 0) {
var lastDot = self.activeDots[self.activeDots.length - 1];
if (firstHitType === "floor") {
lastDot.y = floorCollisionY;
} else if (firstHitType === "fruit" && firstHitFruit) {
// Place the dot just above the hit fruit, using ellipse radii
var yOffset = firstHitFruit.height / 2 + activeFruit.height / 2 + 2;
lastDot.y = firstHitFruit.y - yOffset;
// Clamp to floor if needed
if (lastDot.y > floorCollisionY) {
lastDot.y = floorCollisionY;
}
}
// Hide any extra dots after the intersection
for (var i = self.activeDots.length; i < self.maxDots; i++) {
var extraDot = self.dotPool.get();
if (extraDot) {
extraDot.visible = false;
self.dotPool.release(extraDot);
}
}
}
};
// Ellipse-ellipse intersection for trajectory prediction
self.ellipseEllipseIntersect = function (x1, y1, w1, h1, x2, y2, w2, h2) {
// If both ellipses are circles, use circle distance
if (Math.abs(w1 - h1) < 1 && Math.abs(w2 - h2) < 1) {
var r1 = w1 / 2;
var r2 = w2 / 2;
var dx = x1 - x2;
var dy = y1 - y2;
return dx * dx + dy * dy <= (r1 + r2) * (r1 + r2);
}
// For general ellipses, use a quick approximation:
// Project the distance between centers onto the axis, normalize by radii
var dx = x1 - x2;
var dy = y1 - y2;
var rx = w1 / 2 + w2 / 2;
var ry = h1 / 2 + h2 / 2;
// Normalize the distance
var norm = dx * dx / (rx * rx) + dy * dy / (ry * ry);
return norm <= 1.0;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
// No title, no description
backgroundColor: 0xffe122
});
/****
* Game Code
****/
// Global variable for trajectory line Y offset (relative to fruit's bottom, not its center)
// --- Constants ---
function getTrajectoryLineYOffset(fruit) {
// Offset is from the bottom of the fruit, so half the height plus a small gap (e.g. 50px)
return (fruit && fruit.height ? fruit.height / 2 : 0) + 10;
}
var _GAME_CONSTANTS; // Keep original babel helper structure if present
function _typeof(o) {
/* Babel helper */"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _defineProperty(e, r, t) {
/* Babel helper */
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: !0,
configurable: !0,
writable: !0
}) : e[r] = t, e;
}
function _toPropertyKey(t) {
/* Babel helper */
var i = _toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : i + "";
}
function _toPrimitive(t, r) {
/* Babel helper */
if ("object" != _typeof(t) || !t) {
return t;
}
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) {
return i;
}
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
// Refactored and added constants
var GAME_CONSTANTS = _GAME_CONSTANTS = {
GAME_WIDTH: 2048,
GAME_HEIGHT: 2732,
DROP_POINT_Y: 200,
DROP_START_Y_OFFSET: 200,
// Initial offset above the drop point for visual placement
CLICK_DELAY_MS: 200,
// Reduced for responsiveness
FRUIT_IMMUNITY_MS: 300,
// Shorter immunity after drop
MERGE_GRACE_MS: 100,
// Shorter grace period for merges
MERGE_ANIMATION_DURATION: 150,
// ms for merge shrink animation
GAME_OVER_LINE_Y: 550,
GAME_OVER_COUNTDOWN_MS: 2500,
// Slightly shorter countdown
// --- Simplified Physics Constants ---
BASE_GRAVITY: 2,
// Increased gravity for faster falling fruits
// Adjusted for a different feel, may need tuning with TIMESTEP
GRAVITY_LEVEL_POWER_SCALE: 1.25,
// How much fruit level affects its mass/gravity effect (power)
GRAVITY_LEVEL_MULTIPLIER_ADJUSTED: 0.1,
// Additional multiplier for level-based gravity adjustment
PHYSICS_TIMESTEP_ADJUSTMENT: 1.0,
// If using fixed update, this is 1. If variable, use delta time. For now, 1.
BASE_ELASTICITY: 0.8,
// Base bounciness
MIN_ELASTICITY: 0.1,
// Minimum bounciness for heaviest fruits
ELASTICITY_DECREASE_PER_LEVEL: 0.05,
// How much elasticity decreases per fruit level
AIR_FRICTION_MULTIPLIER: 0.99,
// General damping for linear velocity in air (per frame)
GROUND_LINEAR_FRICTION: 0.92,
// Friction for linear velocity when on ground (per frame by CollisionComponent)
AIR_ANGULAR_DAMPING: 0.98,
// Damping for angular velocity in air
GROUND_CONTACT_ANGULAR_DAMPING: 0.92,
// General angular damping when on ground
GROUND_ANGULAR_DAMPING: 0.85,
// Specific angular damping from floor contact (CollisionComponent)
WALL_BOUNCE_DAMPING: 0.85,
// How much velocity is retained on wall bounce (multiplied by elasticity)
FLOOR_BOUNCE_DAMPING: 0.75,
// How much velocity is retained on floor bounce
WALL_ANGULAR_IMPULSE_FACTOR: 0,
// Small rotation from hitting walls
WALL_ANGULAR_DAMPING: 0.9,
// Damping of angular velocity after wall hit
GROUND_ROLLING_FACTOR: 0.004,
// Lowered to reduce induced spin from rolling
// How much horizontal speed contributes to rolling on ground
MIN_VX_FOR_ROLLING: 0.13,
// Slightly increased to reduce jitter and unnecessary rolling
// Minimum horizontal speed to initiate rolling effect
MAX_LINEAR_VELOCITY_BASE: 50,
MAX_LINEAR_VELOCITY_REDUCTION_PER_LEVEL: 2.0,
MIN_MAX_LINEAR_VELOCITY: 10,
MAX_ANGULAR_VELOCITY_BASE: 0.12,
// Radians per frame
MAX_ANGULAR_VELOCITY_REDUCTION_PER_LEVEL: 0.005,
MIN_MAX_ANGULAR_VELOCITY: 0.03,
ANGULAR_STOP_THRESHOLD_PHYSICS: 0.0008,
// If angular speed is below this, might stop (in PhysicsComponent)
LINEAR_SPEED_SQ_FOR_ANGULAR_STOP: 0.05,
// If linear speed squared is also low, rotation fully stops
RESTING_VELOCITY_THRESHOLD: 0.4,
// Below this vy on floor, vy becomes 0 (in CollisionComponent)
ANGULAR_RESTING_THRESHOLD: 0.005,
// Below this angularV on floor, angularV becomes 0 (in CollisionComponent)
MAX_WALL_CONTACT_FRAMES_FOR_FRICTION_EFFECT: 10,
// Max frames wall contact has minor effect (used by CollisionComponent state)
// Simplified Sleep/Stabilization State Constants
SLEEP_DELAY_FRAMES_SIMPLIFIED: 55,
// Frames of low activity to go to sleep (increased for more robust settling)
STABILIZED_DELAY_FRAMES: 20,
// Frames of low activity to be considered "fully stabilized" (for game over, etc.)
ACTIVE_LINEAR_SPEED_SQ_THRESHOLD: 0.07,
// If linear speed squared is above this, fruit is active
ACTIVE_ANGULAR_SPEED_THRESHOLD: 0.0035,
// If angular speed is above this, fruit is active
ACTIVE_DELTA_V_SQ_THRESHOLD: 0.05,
// If change in velocity squared is high, fruit is active
STABLE_VELOCITY_THRESHOLD: 0.03,
// If velocities are below this when 'isFullyStabilized', they are zeroed out.
STABLE_ANGULAR_VELOCITY_THRESHOLD: 0.0007,
MIN_FALL_SPEED_IF_SLOWING: 0.05,
// Prevents fruits from floating if gravity effect becomes too small mid-air
WAKE_UP_IMPULSE_THRESHOLD_LINEAR: 0.5,
// Min linear impulse from collision to wake up a sleeping fruit
WAKE_UP_IMPULSE_THRESHOLD_ANGULAR: 0.01,
// Min angular impulse from collision to wake up
// Collision Response
FRUIT_COLLISION_SEPARATION_FACTOR: 0.9,
// How much to push fruits apart on overlap (0 to 1)
FRUIT_COLLISION_BASE_MASS_POWER: 1.5,
// Fruit mass = level ^ this_power (for impulse calc)
FRUIT_COLLISION_FRICTION_COEFFICIENT: 0.01,
// Tangential friction between fruits
FRUIT_COLLISION_ROTATION_TRANSFER: 0.005,
// How much tangential collision affects rotation (keep small)
FRUIT_HITBOX_REDUCTION_PER_LEVEL_DIFF: 1.5,
// For `getAdjustedFruitRadius`
BOUNCE_SOUND_VELOCITY_THRESHOLD: 1.0,
// Adjusted threshold for playing bounce sound
// Spatial Grid
SPATIAL_GRID_REBUILD_INTERVAL_MS: 30000,
// Less frequent full rebuilds
SPATIAL_GRID_CELL_SIZE_FACTOR: 1.0,
// Base cell size on average fruit size
// UI & Game Features
CHARGE_NEEDED_FOR_RELEASE: 15,
PORTAL_UI_Y: 120,
PORTAL_UI_X_OFFSET: 870,
PORTAL_TWEEN_DURATION: 300,
PORTAL_PULSE_DURATION: 500,
PINEAPPLE_START_Y: 200,
PINEAPPLE_END_POS_FACTOR: 0.16,
PINEAPPLE_TWEEN_DURATION: 300,
PINEAPPLE_IMMUNITY_MS: 2000,
// Slightly shorter
FIRE_BASE_COUNT: 3,
FIRE_FRUIT_TYPE_THRESHOLD: 1,
FIRE_MAX_COUNT: 15,
FIRE_START_Y_OFFSET: 50,
FIRE_STACK_Y_OFFSET: 100,
FIRE_X_SPREAD: 500,
FIRE_FLICKER_SPEED_BASE: 500,
FIRE_FLICKER_SPEED_RANDOM: 300,
FIRE_ALPHA_MIN: 0.1,
FIRE_ALPHA_MAX: 0.5,
FIRE_FRAME_DURATION: 200,
COCONUT_SPAWN_SCORE_INTERVAL: 500,
EVOLUTION_LINE_Y: 120,
EVOLUTION_ICON_MAX_SIZE: 150,
// Adjusted for potentially smaller screen area
EVOLUTION_ICON_SPACING: 15,
SCORE_TEXT_Y: 400,
// Adjusted for consistency if needed
SCORE_TEXT_SIZE: 120 // Adjusted for consistency
}; // Removed the comma and the comment "/* Babel definitions if any */"
// --- Game Variables ---
var gameOverLine;
var pineapple; // The special pineapple fruit instance
var pineappleActive = false; // If the special pineapple has been dropped
// var pineapplePushCount = 0; // This seems to be replaced by mergeCounter for pineapple logic
var trajectoryLine; // Instance of TrajectoryLine class
var isClickable = true; // Prevents rapid firing of fruits
var evolutionLine; // UI element showing fruit progression
var fireContainer; // Container for fire particle effects
var activeFireElements = []; // Array of active fire Particle instances
var fruitLevels = {
// Defines the "level" or tier of each fruit type
'CHERRY': 1,
'GRAPE': 2,
'APPLE': 3,
'ORANGE': 4,
'WATERMELON': 5,
'PINEAPPLE': 6,
'MELON': 7,
'PEACH': 8,
'COCONUT': 9,
'DURIAN': 10
};
var FruitTypes = {
// Defines properties for each fruit
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'
},
// Note: Orange size was smaller than apple
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
} // No next fruit after Durian
};
var gameWidth = GAME_CONSTANTS.GAME_WIDTH;
var gameHeight = GAME_CONSTANTS.GAME_HEIGHT;
var fruits = []; // Array to hold all active Fruit instances in the game
var nextFruitType = null; // The type of the next fruit to be dropped
var activeFruit = null; // The Fruit instance currently being controlled by the player
var wallLeft, wallRight, gameFloor; // Boundary objects
var dropPointY = GAME_CONSTANTS.DROP_POINT_Y; // Y position where fruits are aimed/dropped from
var gameOver = false; // Game state flag
var scoreText; // Text object for displaying score
var isDragging = false; // If the player is currently dragging the active fruit
// Charged Ball UI / Mechanic
var chargedBallUI = null; // Instance of ChargedBallUI
var chargeCounter = 0; // Counts drops towards charged ball release
var readyToReleaseCharged = false; // Flag if the charged ball is ready
// Merge tracking
var mergeCounter = 0; // Counts merges, used for pineapple release
var lastDroppedFruit = null; // Reference to the last fruit dropped by the player
var lastDroppedHasMerged = false; // Flag if the last dropped fruit has successfully merged
var spatialGrid = null; // Instance of SpatialGrid for collision optimization
var lastScoreCheckForCoconut = 0; // Tracks score for spawning coconuts
// --- Helper Functions ---
function getFruitLevel(fruit) {
if (!fruit || !fruit.type || !fruit.type.id) {
// console.warn("getFruitLevel: Invalid fruit or fruit type. Defaulting to highest level to be safe.");
return 10; // Default to a high level if type is unknown, makes it heavy.
}
var typeId = fruit.type.id.toUpperCase();
if (fruitLevels.hasOwnProperty(typeId)) {
return fruitLevels[typeId];
}
// console.warn("getFruitLevel: Unknown fruit type ID '" + typeId + "'. Defaulting.");
return 10;
}
// Helper to get an "adjusted" radius for collision, potentially smaller for higher level fruits
// This is a simplified version of the original hitbox reduction.
function getAdjustedFruitRadius(fruit) {
var baseRadius = fruit.width / 2; // Assuming width is representative for radius
var level = getFruitLevel(fruit);
// Reduce radius slightly for fruits above a certain level, e.g. level 4+
// var reduction = Math.max(0, level - 3) * GAME_CONSTANTS.FRUIT_HITBOX_REDUCTION_PER_LEVEL_DIFF;
// For simplicity now, just use baseRadius or a fixed small reduction if needed.
// The primary collision logic uses full AABB now, so complex radius adjustment is less critical.
return baseRadius;
}
// --- Game Logic Functions ---
function releasePineappleOnMerge() {
if (mergeCounter >= GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE && !pineappleActive && pineapple) {
pineappleActive = true;
pineapple.isStatic = false; // Allow it to fall
pineapple.immuneToGameOver = true; // Temporary immunity
// --- FIX: Ensure pineapple and all its children are visible and alpha 1 ---
pineapple.alpha = 1.0;
pineapple.visible = true;
if (pineapple.children && pineapple.children.length > 0) {
for (var i = 0; i < pineapple.children.length; i++) {
pineapple.children[i].visible = true;
pineapple.children[i].alpha = 1.0;
}
}
// Defensive: also ensure container itself is visible
pineapple.visible = true;
// Defensive: also ensure scale is correct (sometimes scale can be set to 0 by animation bugs)
pineapple.scaleX = 1;
pineapple.scaleY = 1;
if (pineapple.children && pineapple.children.length > 0) {
for (var i = 0; i < pineapple.children.length; i++) {
pineapple.children[i].scaleX = 1;
pineapple.children[i].scaleY = 1;
}
}
applyDropPhysics(pineapple, 1.5); // Give it a gentle initial push
fruits.push(pineapple);
if (spatialGrid) {
spatialGrid.insertObject(pineapple);
}
LK.setTimeout(function () {
if (pineapple && fruits.indexOf(pineapple) !== -1) {
// Check if it still exists and is in game
pineapple.immuneToGameOver = false;
pineapple.alpha = 1.0; // Ensure pineapple remains visible after immunity wears off
pineapple.visible = true;
if (pineapple.children && pineapple.children.length > 0) {
for (var i = 0; i < pineapple.children.length; i++) {
pineapple.children[i].visible = true;
pineapple.children[i].alpha = 1.0;
pineapple.children[i].scaleX = 1;
pineapple.children[i].scaleY = 1;
}
}
pineapple.scaleX = 1;
pineapple.scaleY = 1;
pineapple.visible = true;
pineappleActive = false; // Reset flag ONLY after pineapple cycle is complete
setupPineapple(); // Create a new static pineapple for the next cycle
mergeCounter = 0; // Reset counter for next pineapple cycle
}
}, GAME_CONSTANTS.PINEAPPLE_IMMUNITY_MS);
}
}
function setupBoundaries() {
// Left Wall
wallLeft = game.addChild(LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
}));
wallLeft.x = 0; // Positioned at the left edge
wallLeft.y = gameHeight / 2;
wallLeft.alpha = 0; // Invisible, but physics active
// Right Wall
wallRight = game.addChild(LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
}));
wallRight.x = gameWidth; // Positioned at the right edge
wallRight.y = gameHeight / 2;
wallRight.alpha = 0;
// Floor
gameFloor = game.addChild(LK.getAsset('floor', {
anchorX: 0.5,
anchorY: 0.5
}));
gameFloor.x = gameWidth / 2;
gameFloor.y = gameHeight; // Positioned at the bottom
gameFloor.alpha = 0;
// Game Over Line
gameOverLine = game.addChild(new Line()); // Uses the Line class
gameOverLine.x = gameWidth / 2;
gameOverLine.y = GAME_CONSTANTS.GAME_OVER_LINE_Y;
gameOverLine.scaleX = gameWidth / 100; // Scale to full screen width (Line asset is 100px wide)
gameOverLine.scaleY = 0.2; // Make it thin
gameOverLine.alpha = 1; // Visible
}
function createNextFruit() {
var fruitProbability = Math.random();
// Determine next fruit type (e.g., mostly cherries, some grapes)
var typeKey = fruitProbability < 0.65 ? 'CHERRY' : fruitProbability < 0.9 ? 'GRAPE' : 'APPLE'; // Added Apple for variety
nextFruitType = FruitTypes[typeKey];
activeFruit = new Fruit(nextFruitType);
// Position above drop point, centered based on last drop or default
activeFruit.x = lastDroppedFruit && lastDroppedFruit.x ? lastDroppedFruit.x : gameWidth / 2;
// The aiming Y position for dragging (where the fruit is shown before drop)
activeFruit.y = dropPointY - GAME_CONSTANTS.DROP_START_Y_OFFSET + 300;
activeFruit.isStatic = true; // Static until dropped
game.addChild(activeFruit);
if (trajectoryLine) {
trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y + getTrajectoryLineYOffset(activeFruit)); // Start trajectory below the fruit
}
}
function dropFruit() {
if (gameOver || !activeFruit || !isClickable) {
return; // Can't drop if game over, no active fruit, or click delay active
}
isClickable = false;
LK.setTimeout(function () {
isClickable = true;
}, GAME_CONSTANTS.CLICK_DELAY_MS);
activeFruit.isStatic = false; // Make it dynamic
// Do NOT reset activeFruit.y here; preserve its current position as set by dragging
applyDropPhysics(activeFruit, 2.0); // Apply initial physics forces (forceMultiplier can be tuned)
fruits.push(activeFruit);
if (spatialGrid) {
spatialGrid.insertObject(activeFruit);
}
lastDroppedFruit = activeFruit;
lastDroppedHasMerged = false; // Reset merge tracking for this new fruit
// Increment charge counter for special ability
chargeCounter++;
updateChargedBallDisplay();
if (chargeCounter >= GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE && !readyToReleaseCharged) {
// releaseChargedBalls(); // This was the old name, new one is setReadyState on UI
chargedBallUI && chargedBallUI.setReadyState(true); // Signal UI it's ready
readyToReleaseCharged = true; // Game logic flag
}
// Grant a temporary grace period for merging
activeFruit.mergeGracePeriodActive = true;
LK.setTimeout(function () {
if (activeFruit && fruits.indexOf(activeFruit) !== -1) {
// Check if fruit still exists
activeFruit.mergeGracePeriodActive = false;
}
}, GAME_CONSTANTS.MERGE_GRACE_MS);
if (trajectoryLine) {
// Clear trajectory line after dropping
trajectoryLine.clearDots();
}
// Reset 'fromChargedRelease' flag for all existing fruits (if it was used)
for (var i = 0; i < fruits.length; i++) {
if (fruits[i] && fruits[i].fromChargedRelease) {
fruits[i].fromChargedRelease = false;
}
}
LK.getSound('drop').play();
// Handle the "charged ball" release (Pickle Rick orange)
if (readyToReleaseCharged && chargeCounter >= GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE) {
LK.getSound('pickleRick').play();
var orange = new Fruit(FruitTypes.ORANGE); // Spawn an orange
// Determine spawn position for the orange
var minX = wallLeft.x + wallLeft.width / 2 + orange.width / 2 + 20;
var maxX = wallRight.x - wallRight.width / 2 - orange.width / 2 - 20;
orange.x = minX + Math.random() * (maxX - minX);
orange.y = -orange.height; // Start off-screen from top
orange.isStatic = false;
applyDropPhysics(orange, 2.5 + (Math.random() * 1.0 - 0.5)); // Give it a varied drop force
orange.fromChargedRelease = true; // Mark it as special
game.addChild(orange);
fruits.push(orange);
if (spatialGrid) {
spatialGrid.insertObject(orange);
}
chargeCounter = 0; // Reset charge
resetChargedBalls(); // Reset UI
readyToReleaseCharged = false; // Reset flag
}
activeFruit = null; // Current fruit is dropped, clear activeFruit
LK.setTimeout(function () {
createNextFruit(); // Prepare the next one after 200ms delay
}, 200);
}
function applyDropPhysics(fruit, forceMultiplier) {
var fruitLevel = getFruitLevel(fruit);
// Base downward force, slightly adjusted by level (heavier fruits fall a bit 'straighter')
var levelAdjustedForce = forceMultiplier * (1 - fruitLevel * 0.03); // Small reduction for higher levels
// Small random horizontal angle for variation
var angleSpread = 15; // degrees
var angle = (Math.random() * angleSpread - angleSpread / 2) * (Math.PI / 180); // radians
fruit.vx = Math.sin(angle) * levelAdjustedForce * 0.5; // Horizontal component is less forceful
fruit.vy = Math.abs(Math.cos(angle) * levelAdjustedForce); // Ensure initial vy is downwards or zero
if (fruit.vy < 0.1) {
fruit.vy = 0.1;
} // Minimum downward push
fruit.angularVelocity = Math.random() * 0.02 - 0.01; // Tiny random initial spin
// Recalculate gravity based on its specific properties if needed, or ensure it's set
fruit.gravity = GAME_CONSTANTS.BASE_GRAVITY * (1 + Math.pow(fruitLevel, GAME_CONSTANTS.GRAVITY_LEVEL_POWER_SCALE) * GAME_CONSTANTS.GRAVITY_LEVEL_MULTIPLIER_ADJUSTED);
// Reset physics states
fruit.isSleeping = false;
fruit.isFullyStabilized = false;
fruit._sleepCounter = 0;
fruit.immuneToGameOver = true; // Temporary immunity
LK.setTimeout(function () {
if (fruit && fruits.indexOf(fruit) !== -1) {
fruit.immuneToGameOver = false;
}
}, GAME_CONSTANTS.FRUIT_IMMUNITY_MS);
}
function updateScoreDisplay() {
if (scoreText) {
// Ensure scoreText is initialized
scoreText.setText(LK.getScore());
}
}
function setupUI() {
// Score Text
scoreText = new Text2("0", {
// Assuming Text2 is a valid class in LK or Upit
size: 200,
fill: 0x000000 // Black color
});
scoreText.anchor.set(0.5, 0.5); // Anchor to center
game.addChild(scoreText); // Add to midground container instead of GUI layer
scoreText.y = gameHeight / 2 - 700; // 800 pixels above center (moved 300px higher)
scoreText.x = gameWidth / 2; // Center horizontally
// Charged Ball Display
setupChargedBallDisplay();
}
function setupChargedBallDisplay() {
if (chargedBallUI) {
// If exists, destroy before creating new
chargedBallUI.destroy();
}
chargedBallUI = new ChargedBallUI();
game.addChild(chargedBallUI); // Add to main game stage
// chargedBallUI.initialize() is called within its constructor
}
function updateChargedBallDisplay() {
if (chargedBallUI) {
chargedBallUI.updateChargeDisplay(chargeCounter);
}
}
// function releaseChargedBalls() { // This logic is now more integrated
// readyToReleaseCharged = true;
// if (chargedBallUI) {
// chargedBallUI.setReadyState(true);
// }
// }
function resetChargedBalls() {
if (chargedBallUI) {
chargedBallUI.reset();
}
readyToReleaseCharged = false; // Also reset the game logic flag
chargeCounter = 0; // Reset the counter itself
updateChargedBallDisplay(); // Update UI to reflect reset
}
function checkFruitCollisions() {
// Reset neighbor/surrounded counts for all fruits before collision checks
for (var k = 0; k < fruits.length; k++) {
var fruitInstance = fruits[k];
if (fruitInstance) {
fruitInstance.neighborContacts = []; // Reset list of direct contacts
// fruitInstance.surroundedFrames = 0; // This might be better managed by PhysicsComponent based on continuous contact
}
}
outerLoop: for (var i = fruits.length - 1; i >= 0; i--) {
var fruit1 = fruits[i];
if (!fruit1 || fruit1 === activeFruit || fruit1.merging || fruit1.isStatic || fruit1.isSleeping) {
// Sleeping fruits don't initiate active collision response
continue;
}
var candidates = spatialGrid.getPotentialCollisions(fruit1);
for (var j = 0; j < candidates.length; j++) {
var fruit2 = candidates[j];
// Basic checks to ensure fruit2 is valid for collision
if (!fruit2 || fruit2 === activeFruit || fruit2.merging || fruit2.isStatic || fruit1 === fruit2) {
continue;
}
// Ensure fruit2 is actually in the main fruits array (sanity check)
if (fruits.indexOf(fruit2) === -1) {
continue;
}
// --- Collision Detection (Ellipse-Ellipse for merging, AABB for physics) ---
var f1HalfWidth = fruit1.width / 2;
var f1HalfHeight = fruit1.height / 2;
var f2HalfWidth = fruit2.width / 2;
var f2HalfHeight = fruit2.height / 2;
var dx = fruit2.x - fruit1.x;
var dy = fruit2.y - fruit1.y;
// Use ellipse-ellipse intersection for merging (touch = merge)
var shouldMerge = false;
if (fruit1.type.id === fruit2.type.id) {
// Use the same ellipseEllipseIntersect as TrajectoryLine
// Defensive: check if TrajectoryLine exists, else define a local function
var ellipseEllipseIntersect = typeof TrajectoryLine !== "undefined" && TrajectoryLine.prototype && TrajectoryLine.prototype.ellipseEllipseIntersect ? TrajectoryLine.prototype.ellipseEllipseIntersect : function (x1, y1, w1, h1, x2, y2, w2, h2) {
// If both ellipses are circles, use circle distance
if (Math.abs(w1 - h1) < 1 && Math.abs(w2 - h2) < 1) {
var r1 = w1 / 2;
var r2 = w2 / 2;
var dx = x1 - x2;
var dy = y1 - y2;
return dx * dx + dy * dy <= (r1 + r2) * (r1 + r2);
}
// For general ellipses, use a quick approximation:
var dx = x1 - x2;
var dy = y1 - y2;
var rx = w1 / 2 + w2 / 2;
var ry = h1 / 2 + h2 / 2;
var norm = dx * dx / (rx * rx) + dy * dy / (ry * ry);
return norm <= 1.0;
};
shouldMerge = ellipseEllipseIntersect(fruit1.x, fruit1.y, fruit1.width, fruit1.height, fruit2.x, fruit2.y, fruit2.width, fruit2.height);
}
// Use AABB for physics collision response (as before)
var collidingX = Math.abs(dx) < f1HalfWidth + f2HalfWidth;
var collidingY = Math.abs(dy) < f1HalfHeight + f2HalfHeight;
if (collidingX && collidingY) {
// Wake up sleeping fruits if they are involved in a collision
if (fruit1.isSleeping) {
fruit1.wakeUp();
}
if (fruit2.isSleeping) {
fruit2.wakeUp();
}
// Track neighbor contacts
if (fruit1.neighborContacts.indexOf(fruit2.id) === -1) {
fruit1.neighborContacts.push(fruit2.id);
}
if (fruit2.neighborContacts.indexOf(fruit1.id) === -1) {
fruit2.neighborContacts.push(fruit1.id);
}
// --- Merge Check ---
// Identical fruits should always merge when they touch - don't do physics collision
if (fruit1.type.id === fruit2.type.id && shouldMerge) {
fruit1.merge(fruit2); // Delegate to fruit's merge component
continue outerLoop; // Critical: restart outer loop as fruit1 (and fruit2) might be gone
}
// --- Collision Response (Simplified Impulse-Based) ---
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance === 0) {
// Prevent division by zero if perfectly overlapped
distance = 0.1;
dx = 0.1; // Arbitrary small separation
}
var normalX = dx / distance;
var normalY = dy / distance;
// 1. Separation (Resolve Overlap)
// Calculate overlap based on AABB - this is a simplification.
// For circular, it's (r1+r2) - distance. Here, use an estimate.
var overlapX = f1HalfWidth + f2HalfWidth - Math.abs(dx);
var overlapY = f1HalfHeight + f2HalfHeight - Math.abs(dy);
var overlap = Math.min(overlapX, overlapY) * GAME_CONSTANTS.FRUIT_COLLISION_SEPARATION_FACTOR; // Use smaller overlap dimension
if (overlap > 0) {
// Distribute separation based on "mass" (derived from level)
var level1 = getFruitLevel(fruit1);
var level2 = getFruitLevel(fruit2);
// Reduce the mass power to make heavier fruits resist being pushed more (was 1.5, now 1.1)
var mass1 = Math.pow(level1, 1.1) || 1;
var mass2 = Math.pow(level2, 1.1) || 1;
var totalMass = mass1 + mass2;
// --- Emphasize vertical resolution ---
// Compute a bias to favor vertical (downward/upward) separation
// If fruit1 is below fruit2, bias the separation vector more vertical for fruit1 (and vice versa)
var verticalBias = 0.33; // 0 = no bias, 1 = full vertical, 0.33 is a stronger nudge
var sepNormalX = normalX;
var sepNormalY = normalY;
if (Math.abs(normalY) > 0.5) {
// If the collision is already mostly vertical, keep as is
sepNormalX = normalX;
sepNormalY = normalY;
} else {
// If mostly horizontal, blend in more vertical
if (fruit1.y > fruit2.y) {
// fruit1 is below, bias its separation more upward
sepNormalX = normalX * (1 - verticalBias);
sepNormalY = normalY * (1 - verticalBias) - verticalBias;
} else {
// fruit1 is above, bias its separation more downward
sepNormalX = normalX * (1 - verticalBias);
sepNormalY = normalY * (1 - verticalBias) + verticalBias;
}
// Normalize the separation vector
var sepLen = Math.sqrt(sepNormalX * sepNormalX + sepNormalY * sepNormalY);
if (sepLen > 0) {
sepNormalX /= sepLen;
sepNormalY /= sepLen;
}
}
// Reduce separation factor to prevent excessive push
var separation1 = mass2 / totalMass * overlap * 0.7;
var separation2 = mass1 / totalMass * overlap * 0.7;
fruit1.x -= sepNormalX * separation1;
fruit1.y -= sepNormalY * separation1;
fruit2.x += sepNormalX * separation2;
fruit2.y += sepNormalY * separation2;
}
// 2. Impulse (Exchange Momentum)
var relVx = fruit2.vx - fruit1.vx;
var relVy = fruit2.vy - fruit1.vy;
var contactVelocity = relVx * normalX + relVy * normalY;
if (contactVelocity < 0) {
// If objects are moving towards each other
var combinedElasticity = Math.min(fruit1.elasticity, fruit2.elasticity); // Or average, or max
// Reduce impulse magnitude to prevent jerky movement and tunneling
var impulseMagnitude = -(1 + combinedElasticity) * contactVelocity * 0.5; // Reduced from 0.7 to 0.5
// Cap maximum impulse magnitude to prevent extreme forces causing unnatural movement
var maxImpulse = 3.0; // Maximum allowed impulse magnitude
impulseMagnitude = Math.min(Math.abs(impulseMagnitude), maxImpulse) * (impulseMagnitude < 0 ? -1 : 1);
// Distribute impulse based on mass
var impulseRatio1 = mass2 / totalMass;
var impulseRatio2 = mass1 / totalMass;
// Calculate impulse vectors with distance-based scaling
var distanceScale = Math.min(1.0, Math.max(0.2, distance / (fruit1.width + fruit2.width))); // Scale by relative distance
fruit1.vx -= impulseMagnitude * normalX * impulseRatio1 * distanceScale;
fruit1.vy -= impulseMagnitude * normalY * impulseRatio1 * distanceScale;
fruit2.vx += impulseMagnitude * normalX * impulseRatio2 * distanceScale;
fruit2.vy += impulseMagnitude * normalY * impulseRatio2 * distanceScale;
// Wake up fruits if impulse is significant
if (Math.abs(impulseMagnitude * impulseRatio1) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_LINEAR) {
fruit1.wakeUp();
}
if (Math.abs(impulseMagnitude * impulseRatio2) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_LINEAR) {
fruit2.wakeUp();
}
// 3. Friction (Tangential Impulse) - Simplified
var tangentX = -normalY;
var tangentY = normalX;
var tangentVelocity = relVx * tangentX + relVy * tangentY;
// Make friction coefficient stronger when normal force (contactVelocity) is higher
var frictionBase = GAME_CONSTANTS.FRUIT_COLLISION_FRICTION_COEFFICIENT;
var frictionBoost = Math.min(Math.abs(contactVelocity) * 0.12, 0.18); // Up to +0.18 for strong contacts
var frictionImpulseMag = -tangentVelocity * (frictionBase + frictionBoost);
fruit1.vx -= frictionImpulseMag * tangentX * impulseRatio1;
fruit1.vy -= frictionImpulseMag * tangentY * impulseRatio1;
fruit2.vx += frictionImpulseMag * tangentX * impulseRatio2;
fruit2.vy += frictionImpulseMag * tangentY * impulseRatio2;
// 4. Rotational Impulse (very simplified, can be made more complex if needed)
// This makes fruits spin a little on glancing blows
var angularImpulse = tangentVelocity * GAME_CONSTANTS.FRUIT_COLLISION_ROTATION_TRANSFER;
// Distribute angular impulse (inverse of linear mass ratio for inertia approximation)
// This is a rough approximation. True rotational inertia is more complex.
var angImpRatio1 = impulseRatio2; // Fruit with less "linear push back" gets more spin
var angImpRatio2 = impulseRatio1;
fruit1.angularVelocity -= angularImpulse * angImpRatio1;
fruit2.angularVelocity += angularImpulse * angImpRatio2;
if (Math.abs(angularImpulse * angImpRatio1) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_ANGULAR) {
fruit1.wakeUp();
}
if (Math.abs(angularImpulse * angImpRatio2) > GAME_CONSTANTS.WAKE_UP_IMPULSE_THRESHOLD_ANGULAR) {
fruit2.wakeUp();
}
// Immediately apply significant angular damping after collision to both fruits
fruit1.angularVelocity *= 0.85;
fruit2.angularVelocity *= 0.85;
// 5. Rotational Friction (Frictional force to reduce relative angular velocities)
var angularDifference = fruit1.angularVelocity - fruit2.angularVelocity;
// Make frictionStrength stronger when relative linear velocity is low (grinding, not impact)
var relLinearVel = Math.sqrt(relVx * relVx + relVy * relVy);
var frictionStrength = relLinearVel < 0.2 ? 0.08 : 0.03; // Stronger friction if nearly at rest
fruit1.angularVelocity -= angularDifference * frictionStrength;
fruit2.angularVelocity += angularDifference * frictionStrength;
// Optionally, apply a little extra damping when in contact
fruit1.angularVelocity *= 0.97;
fruit2.angularVelocity *= 0.97;
}
}
}
}
}
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 || fruit.immuneToGameOver) {
continue;
}
// Calculate effective top Y of the fruit, considering rotation (simplified)
var fruitHalfHeight = fruit.height / 2;
var cosAngle = Math.abs(Math.cos(fruit.rotation));
var sinAngle = Math.abs(Math.sin(fruit.rotation)); // Using width for sin component of height projection
var effectiveProjectedHeight = fruitHalfHeight * cosAngle + fruit.width / 2 * sinAngle;
var fruitTopY = fruit.y - effectiveProjectedHeight;
if (fruitTopY <= GAME_CONSTANTS.GAME_OVER_LINE_Y) {
// A fruit is above or touching the game over line.
// Now check if it's "stable" or "settled" there.
// Stable means low velocity AND either fully stabilized or sleeping.
var isMovingSlowly = Math.abs(fruit.vy) < 0.5 && Math.abs(fruit.vx) < 0.5; // Tune these thresholds
var isConsideredSettled = isMovingSlowly && (fruit.isFullyStabilized || fruit.isSleeping);
// If it's also somewhat still (e.g., low vy or stabilized state)
if (isConsideredSettled || fruit.wallContactFrames > 0 && isMovingSlowly) {
// Wall contact implies it might be stuck
if (!fruit.gameOverTimer) {
// Start timer if not already started
fruit.gameOverTimer = Date.now();
// Optional: Visual feedback like blinking
tween(fruit, {
alpha: 0.5
}, {
duration: 200,
yoyo: true,
repeat: 5,
easing: tween.easeInOut
});
}
// Check if timer has exceeded countdown
if (Date.now() - fruit.gameOverTimer >= GAME_CONSTANTS.GAME_OVER_COUNTDOWN_MS) {
gameOver = true;
LK.showGameOver(); // Assumes LK provides this
return; // Exit loop and function
}
} else {
// If fruit is above line but moving significantly, reset its timer
if (fruit.gameOverTimer) {
fruit.gameOverTimer = null;
fruit.alpha = 1.0; // Reset visual feedback
}
}
} else {
// Fruit is below the line, reset its timer if it had one
if (fruit.gameOverTimer) {
fruit.gameOverTimer = null;
fruit.alpha = 1.0; // Reset visual feedback
}
}
}
}
function setupPineapple() {
if (pineapple) {
pineapple.destroy();
} // Remove old one if exists
pineapple = new Fruit(FruitTypes.PINEAPPLE);
pineapple.x = -pineapple.width + 350; // Start 200px further to the right than before
pineapple.y = GAME_CONSTANTS.PINEAPPLE_START_Y;
pineapple.isStatic = true; // Static until activated
pineappleActive = false; // Not yet dropped
// pineapplePushCount = 0; // Reset if this counter is used elsewhere
game.addChild(pineapple);
}
function pushPineapple() {
// Always animate the pineapple moving a bit to the right after every merge
if (!pineappleActive && pineapple) {
// Move pineapple a bit to the right for each merge, up to a max position
var minX = -pineapple.width; // Off-screen left
var maxX = gameWidth * GAME_CONSTANTS.PINEAPPLE_END_POS_FACTOR; // Target resting X
// Each merge pushes pineapple a bit to the right
var mergesNeeded = GAME_CONSTANTS.CHARGE_NEEDED_FOR_RELEASE;
// Use Math.min to clamp mergeCounter to mergesNeeded
var progress = Math.min(mergeCounter, mergesNeeded) / mergesNeeded;
var newX = minX + progress * (maxX - minX);
// Stop any existing tween to avoid conflicts
tween.stop(pineapple, {
x: true
});
// Create a new tween with the updated position
tween(pineapple, {
x: newX
}, {
duration: GAME_CONSTANTS.PINEAPPLE_TWEEN_DURATION,
easing: tween.bounceOut // Or a smoother ease like tween.easeOut
});
}
}
function initGame() {
LK.setScore(0);
gameOver = false;
// Clear existing fruits
for (var i = fruits.length - 1; i >= 0; i--) {
if (fruits[i]) {
if (spatialGrid) {
spatialGrid.removeObject(fruits[i]);
}
fruits[i].destroy();
}
}
fruits = [];
// Reset UI and game state variables
if (chargedBallUI) {
chargedBallUI.destroy();
} // Destroy if exists
chargedBallUI = null; // Ensure it's null before recreating
chargeCounter = 0;
readyToReleaseCharged = false;
lastScoreCheckForCoconut = 0;
lastDroppedFruit = null;
lastDroppedHasMerged = false;
mergeCounter = 0;
isClickable = true;
// Clear fire elements
if (fireContainer) {
for (var i = activeFireElements.length - 1; i >= 0; i--) {
if (activeFireElements[i]) {
activeFireElements[i].destroy();
}
}
fireContainer.destroy(); // Destroy the container itself
}
fireContainer = new Container(); // Create a new one
game.addChildAt(fireContainer, 0); // Add to bottom of display stack
activeFireElements = [];
// Initialize Spatial Grid
if (spatialGrid) {
spatialGrid.clear();
} else {
var avgFruitSize = 0;
var fruitTypeCount = 0;
for (var typeKey in FruitTypes) {
avgFruitSize += FruitTypes[typeKey].size;
fruitTypeCount++;
}
avgFruitSize = fruitTypeCount > 0 ? avgFruitSize / fruitTypeCount : 300; // Default if no types
var cellSize = Math.ceil(avgFruitSize * GAME_CONSTANTS.SPATIAL_GRID_CELL_SIZE_FACTOR);
spatialGrid = new SpatialGrid(cellSize);
}
// Destroy and recreate boundaries and UI elements
if (wallLeft) {
wallLeft.destroy();
}
if (wallRight) {
wallRight.destroy();
}
if (gameFloor) {
gameFloor.destroy();
}
if (gameOverLine) {
gameOverLine.destroy();
}
if (pineapple) {
pineapple.destroy();
} // Special pineapple
if (trajectoryLine) {
trajectoryLine.destroy();
}
if (evolutionLine) {
evolutionLine.destroy();
}
if (scoreText) {
// If scoreText was added to LK.gui.top
LK.gui.top.removeChild(scoreText); // Remove from specific GUI layer
scoreText.destroy(); // Then destroy the text object itself
scoreText = null; // Nullify reference
}
LK.playMusic('bgmusic'); // Restart music
setupBoundaries();
setupUI(); // This will create scoreText and chargedBallUI
setupPineapple(); // Creates the initial static pineapple
updateFireBackground(); // Initial fire setup
trajectoryLine = game.addChild(new TrajectoryLine());
// trajectoryLine.createDots(); // Called by constructor or as needed by updateTrajectory
evolutionLine = game.addChild(new EvolutionLine());
// evolutionLine.initialize(); // Called by its constructor
updateScoreDisplay(); // Set score to 0 initially
activeFruit = null; // No fruit being controlled yet
createNextFruit(); // Prepare the first fruit
resetChargedBalls(); // Ensure charge UI is reset
}
function spawnCoconut() {
var coconut = new Fruit(FruitTypes.COCONUT);
var minX = wallLeft.x + wallLeft.width / 2 + coconut.width / 2 + 30; // Buffer from wall
var maxX = wallRight.x - wallRight.width / 2 - coconut.width / 2 - 30;
coconut.x = minX + Math.random() * (maxX - minX);
coconut.y = gameHeight + coconut.height; // Start below screen
coconut.isStatic = true; // Initially static for tweening
game.addChild(coconut);
// Do not add to fruits array or spatial grid until it becomes dynamic
coconut.immuneToGameOver = true; // Immune during spawn animation
var targetY = gameHeight - gameFloor.height / 2 - coconut.height / 2 - 10; // Target just above floor
tween(coconut, {
y: targetY
}, {
duration: 1200,
// Tween duration
easing: tween.easeOut,
// Smoother easing for arrival
onFinish: function onFinish() {
if (!coconut || coconut.parent === null) {
return;
} // Check if still exists
coconut.isStatic = false; // Now it's dynamic
fruits.push(coconut); // Add to main simulation arrays
if (spatialGrid) {
spatialGrid.insertObject(coconut);
}
coconut.vy = -1.5; // Small upward pop
coconut.vx = (Math.random() * 2 - 1) * 1.0; // Small random horizontal push
LK.setTimeout(function () {
if (coconut && fruits.indexOf(coconut) !== -1) {
coconut.immuneToGameOver = false;
}
}, 1000); // Immunity wears off
}
});
}
// --- Player Input ---
game.down = function (x, y) {
if (activeFruit && !gameOver && isClickable) {
// Check isClickable here too for initial press
isDragging = true;
game.move(x, y); // Process initial position
}
};
game.move = function (x, y) {
if (isDragging && activeFruit && !gameOver) {
var fruitRadius = activeFruit.width / 2;
// Constrain X position within walls
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));
// Y position is fixed for aiming
activeFruit.y = dropPointY - GAME_CONSTANTS.DROP_START_Y_OFFSET + 300;
if (trajectoryLine) {
trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y + getTrajectoryLineYOffset(activeFruit)); // Start trajectory below the fruit
}
}
};
game.up = function () {
if (isDragging && activeFruit && isClickable && !gameOver) {
dropFruit();
}
isDragging = false; // Reset dragging state regardless
};
// --- Main Game Loop ---
function updatePhysicsMain() {
// Renamed from updatePhysics to avoid conflict with Fruit.updatePhysics
// Rebuild spatial grid periodically (e.g., if fruits stop moving but grid needs refresh)
// More robust would be to rebuild if many fruits are marked dirty or after many removals.
if (spatialGrid && Date.now() - spatialGrid.lastRebuildTime > spatialGrid.rebuildInterval) {
spatialGrid.rebuildGrid(fruits);
}
// Phase 1: Apply physics forces and update velocities/positions for each fruit
for (var i = fruits.length - 1; i >= 0; i--) {
var fruit = fruits[i];
if (!fruit || fruit.isStatic || fruit.merging) {
continue;
}
fruit.updatePhysics(); // Call the fruit's own physics update method
}
// Phase 2: Check and resolve fruit-to-fruit collisions
checkFruitCollisions(); // Global function for inter-fruit collisions
// Phase 3: Check and resolve boundary collisions, then finalize positions and grid updates
for (var i = fruits.length - 1; i >= 0; i--) {
var fruit = fruits[i];
if (!fruit || fruit.isStatic || fruit.merging) {
continue;
}
var walls = {
left: wallLeft,
right: wallRight
};
fruit.checkBoundaries(walls, gameFloor); // Fruit's method for boundary checks
// Final check for very small velocities if fruit is stabilized or on ground to help it fully stop
if ((fruit.isFullyStabilized || fruit._boundaryContacts && fruit._boundaryContacts.floor) && !fruit.isSleeping) {
if (Math.abs(fruit.vx) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD * 0.5) {
fruit.vx = 0;
}
if (Math.abs(fruit.vy) < GAME_CONSTANTS.STABLE_VELOCITY_THRESHOLD * 0.5) {
fruit.vy = 0;
}
if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.STABLE_ANGULAR_VELOCITY_THRESHOLD * 0.5) {
fruit.angularVelocity = 0;
}
}
// Update fruit's position in the spatial grid after all movements and collision resolutions
if (spatialGrid) {
spatialGrid.updateObject(fruit);
}
}
// Phase 4: (Original) Environmental interactions and position adjustments
// The "gap below" logic was very complex and might be better handled by more robust general physics.
// For now, this is removed to simplify. If fruits get stuck, simpler nudging can be added.
}
game.update = function () {
if (gameOver) {
return; // Stop updates if game over
}
// Coconut spawning logic
var currentScore = LK.getScore();
if (currentScore >= lastScoreCheckForCoconut + GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) {
// Update score checkpoint based on actual score and interval, to prevent multiple spawns if score jumps high
lastScoreCheckForCoconut = Math.floor(currentScore / GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) * GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL;
spawnCoconut();
}
// This else-if ensures that if the score doesn't trigger a spawn, but surpasses the last checkpoint,
// the checkpoint is updated. This handles cases where score might increase by less than the interval.
// However, the above line `lastScoreCheckForCoconut = Math.floor(...)` already handles this.
// else if (currentScore > lastScoreCheckForCoconut) {
// lastScoreCheckForCoconut = Math.floor(currentScore / GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL) * GAME_CONSTANTS.COCONUT_SPAWN_SCORE_INTERVAL;
// }
updatePhysicsMain(); // Main physics and collision update loop
checkGameOver(); // Check game over conditions
// Update visual effects like fire
updateFireBackground();
for (var i = 0; i < activeFireElements.length; i++) {
if (activeFireElements[i]) {
activeFireElements[i].update(); // Assuming FireElement has an update method
}
}
};
// Initialize and start the game
initGame();
// --- Fire Background Logic ---
function updateFireBackground() {
var uniqueFruitTypes = {};
for (var i = 0; i < fruits.length; i++) {
var fruit = fruits[i];
// Consider only dynamic, non-merging fruits with valid types
if (fruit && !fruit.isStatic && !fruit.merging && fruit.type && fruit.type.id) {
uniqueFruitTypes[fruit.type.id] = true;
}
}
var uniqueTypeCount = Object.keys(uniqueFruitTypes).length;
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);
// Add new fire elements if needed
while (activeFireElements.length < targetFireCount) {
createFireElement(activeFireElements.length); // Pass current count as index for positioning
}
// Remove excess fire elements
while (activeFireElements.length > targetFireCount) {
var fireToRemove = activeFireElements.pop();
if (fireToRemove) {
fireToRemove.destroy(); // Ensure proper cleanup of the fire element
// fireContainer.removeChild(fireToRemove); // Destroy should handle this if it's a PIXI object
}
}
}
function createFireElement(index) {
// Calculate position based on index to stack them or spread them out
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); // Assuming FireElement constructor sets up PIXI parts
if (index % 2 === 1) {
// Alternate facing direction for visual variety
if (newFire.fireAsset) {
newFire.fireAsset.scaleX = -1;
}
if (newFire.fireAsset2) {
newFire.fireAsset2.scaleX = -1;
}
}
fireContainer.addChildAt(newFire, 0); // Add to the bottom of the fire container for layering
activeFireElements.push(newFire);
}
function removeFruitFromGame(fruit) {
if (!fruit) {
return;
}
// Wake up any sleeping neighbors that might have been resting on this fruit.
// This is a simplified approach. More robust would be to check if they *were* supported.
if (fruit.neighborContacts && fruit.neighborContacts.length > 0) {
for (var i = 0; i < fruit.neighborContacts.length; i++) {
var neighborId = fruit.neighborContacts[i];
for (var j = 0; j < fruits.length; j++) {
if (fruits[j] && fruits[j].id === neighborId) {
if (fruits[j].isSleeping || fruits[j].isFullyStabilized) {
fruits[j].wakeUp(); // Use the fruit's own wakeUp method
// Optionally give a very small nudge to ensure physics re-evaluates
fruits[j].vy -= 0.05; // Tiny upward nudge
fruits[j].vx += (Math.random() - 0.5) * 0.1; // Tiny sideways nudge
}
break;
}
}
}
}
var index = fruits.indexOf(fruit);
if (index !== -1) {
fruits.splice(index, 1);
}
if (spatialGrid) {
spatialGrid.removeObject(fruit);
}
// If the fruit being removed was the 'lastDroppedFruit', nullify the reference
if (lastDroppedFruit === fruit) {
lastDroppedFruit = null;
}
// If it was the active aiming fruit (shouldn't happen if physics are involved, but defensive)
if (activeFruit === fruit) {
activeFruit = null;
// Potentially createNextFruit() here if game logic requires immediate replacement
}
fruit.destroy(); // Call the fruit's own destroy method (should handle PIXI cleanup)
}