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; } self._pendingTween = { scaleX: targetScale, scaleY: targetScale, alpha: progressPercent, duration: GAME_CONSTANTS.PORTAL_TWEEN_DURATION, easing: tween.easeOut }; self._tweenSignal = true; if (remainingCount === 0 && !self.isReadyToRelease) { self.setReadyState(true); } }; self.setReadyState = function (isReady) { self.isReadyToRelease = isReady; if (isReady) { self._pendingTween = { scaleX: 1.0, scaleY: 1.0, alpha: 1.0, rotation: Math.PI * 2, duration: GAME_CONSTANTS.PORTAL_TWEEN_DURATION, easing: tween.easeOut }; self._tweenSignal = true; self.startPulseAnimation(); } }; self.startPulseAnimation = function () { if (self.pulseAnimationActive) { return; } self.pulseAnimationActive = true; self._pulseText(); }; self._pulseText = function () { if (!self.isReadyToRelease) { self.pulseAnimationActive = false; return; } self._pendingPulseTween = { 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; } self._pendingPulseTween = { scaleX: 1.0, scaleY: 1.0, duration: GAME_CONSTANTS.PORTAL_PULSE_DURATION, easing: tween.easeInOut, onFinish: self._pulseText }; self._pulseTweenSignal = true; } }; self._pulseTweenSignal = true; }; self.reset = function () { self.isReadyToRelease = false; self.currentCharge = 0; self.pulseAnimationActive = false; self._pendingTween = { alpha: 0, scaleX: 0, scaleY: 0, duration: 200, easing: tween.easeOut }; self._tweenSignal = true; }; 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._pendingFlickerTween = { alpha: self.alphaMax, duration: self.flickerSpeed, easing: tween.easeInOut, onFinish: self.flickerToMin }; self._flickerTweenSignal = true; }; self.flickerToMin = function () { self._pendingFlickerTween = { alpha: self.alphaMin, duration: self.flickerSpeed, easing: tween.easeInOut, onFinish: self.flickerToMax }; self._flickerTweenSignal = true; }; 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 mergeCounter++; // Only pushPineapple if the pineapple is not active, exists, and mergeCounter is within threshold if (!pineappleActive && pineapple && mergeCounter <= GAME_CONSTANTS.PINEAPPLE_MERGES_NEEDED) { 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, but we always remove both fruits here for clarity. removeFruitFromGame(fruit1); // Global removal function 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 *= GAME_CONSTANTS.AIR_ANGULAR_DAMPING_EXTRA; // Extra angular friction in air to help stop spinning if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.AIR_ANGULAR_FRICTION_THRESHOLD) { fruit.angularVelocity *= GAME_CONSTANTS.AIR_ANGULAR_FRICTION_MULTIPLIER; // Increased friction } // Extra linear friction in air to help stop drifting if (Math.abs(fruit.vx) < GAME_CONSTANTS.AIR_LINEAR_FRICTION_THRESHOLD) { fruit.vx *= GAME_CONSTANTS.AIR_LINEAR_FRICTION_MULTIPLIER; // Increased friction } if (Math.abs(fruit.vy) < GAME_CONSTANTS.AIR_LINEAR_FRICTION_THRESHOLD) { fruit.vy *= GAME_CONSTANTS.AIR_LINEAR_FRICTION_MULTIPLIER; // Increased friction } // If resting on another fruit, apply extra friction to help it come to rest if (restingOnOtherFruit) { fruit.vx *= GAME_CONSTANTS.RESTING_ON_FRUIT_LINEAR_FRICTION; // Stronger friction fruit.vy *= GAME_CONSTANTS.RESTING_ON_FRUIT_LINEAR_FRICTION; fruit.angularVelocity *= GAME_CONSTANTS.RESTING_ON_FRUIT_ANGULAR_FRICTION; // If velocities are very small, damp even more if (Math.abs(fruit.vx) < GAME_CONSTANTS.RESTING_ON_FRUIT_LINEAR_FRICTION_THRESHOLD) { fruit.vx *= GAME_CONSTANTS.RESTING_ON_FRUIT_LINEAR_FRICTION_EXTRA; } if (Math.abs(fruit.vy) < GAME_CONSTANTS.RESTING_ON_FRUIT_LINEAR_FRICTION_THRESHOLD) { fruit.vy *= GAME_CONSTANTS.RESTING_ON_FRUIT_LINEAR_FRICTION_EXTRA; } if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.RESTING_ON_FRUIT_ANGULAR_FRICTION_THRESHOLD) { fruit.angularVelocity *= GAME_CONSTANTS.RESTING_ON_FRUIT_ANGULAR_FRICTION_EXTRA; } } } 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 *= GAME_CONSTANTS.GROUND_CONTACT_ANGULAR_DAMPING_EXTRA; // 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) < GAME_CONSTANTS.GROUND_CONTACT_LINEAR_VELOCITY_THRESHOLD && Math.abs(fruit.vy) < GAME_CONSTANTS.GROUND_CONTACT_LINEAR_VELOCITY_THRESHOLD) { fruit.angularVelocity *= GAME_CONSTANTS.GROUND_CONTACT_ANGULAR_DAMPING_STRONG; // Very strong damping when nearly at rest on a surface } // Extra angular friction on ground to help stop spinning if (Math.abs(fruit.angularVelocity) < GAME_CONSTANTS.GROUND_CONTACT_ANGULAR_FRICTION_THRESHOLD) { fruit.angularVelocity *= GAME_CONSTANTS.GROUND_CONTACT_ANGULAR_FRICTION_MULTIPLIER; // Even more friction } // Extra linear friction on ground to help stop sliding if (Math.abs(fruit.vx) < GAME_CONSTANTS.GROUND_CONTACT_LINEAR_FRICTION_THRESHOLD) { fruit.vx *= GAME_CONSTANTS.GROUND_CONTACT_LINEAR_FRICTION_MULTIPLIER; // Increased friction } if (Math.abs(fruit.vy) < GAME_CONSTANTS.GROUND_CONTACT_LINEAR_FRICTION_THRESHOLD) { fruit.vy *= GAME_CONSTANTS.GROUND_CONTACT_LINEAR_FRICTION_MULTIPLIER; // 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 (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 // Use global ellipseEllipseIntersect function directly in updateTrajectory return self; }); /**** * Initialize Game ****/ // ---- GLOBAL HELPER FUNCTION ---- var game = new LK.Game({ // No title, no description backgroundColor: 0xffe122 }); /**** * Game Code ****/ // ---- GLOBAL HELPER FUNCTION ---- // --- Constants --- // Global variable for trajectory line Y offset (relative to fruit's bottom, not its center) function ellipseEllipseIntersect(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; } 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, PINEAPPLE_MERGES_NEEDED: 15, // Specific constant for pineapple release 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 // --- New physics/collision constants for magic numbers --- AIR_ANGULAR_DAMPING_EXTRA: 0.995, AIR_ANGULAR_FRICTION_THRESHOLD: 0.01, AIR_ANGULAR_FRICTION_MULTIPLIER: 0.90, AIR_LINEAR_FRICTION_THRESHOLD: 0.05, AIR_LINEAR_FRICTION_MULTIPLIER: 0.90, RESTING_ON_FRUIT_LINEAR_FRICTION: 0.85, RESTING_ON_FRUIT_ANGULAR_FRICTION: 0.85, RESTING_ON_FRUIT_LINEAR_FRICTION_THRESHOLD: 0.02, RESTING_ON_FRUIT_LINEAR_FRICTION_EXTRA: 0.7, RESTING_ON_FRUIT_ANGULAR_FRICTION_THRESHOLD: 0.005, RESTING_ON_FRUIT_ANGULAR_FRICTION_EXTRA: 0.7, GROUND_CONTACT_ANGULAR_DAMPING_EXTRA: 0.88, GROUND_CONTACT_LINEAR_VELOCITY_THRESHOLD: 0.08, GROUND_CONTACT_ANGULAR_DAMPING_STRONG: 0.7, GROUND_CONTACT_ANGULAR_FRICTION_THRESHOLD: 0.01, GROUND_CONTACT_ANGULAR_FRICTION_MULTIPLIER: 0.75, GROUND_CONTACT_LINEAR_FRICTION_THRESHOLD: 0.05, GROUND_CONTACT_LINEAR_FRICTION_MULTIPLIER: 0.80 }; // 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.PINEAPPLE_MERGES_NEEDED && !pineappleActive && pineapple) { pineappleActive = true; // Set this FIRST // --- 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 --- // Capture THIS specific pineapple instance before any changes var releasedPineapple = pineapple; releasedPineapple.isStatic = false; // Allow it to fall releasedPineapple.immuneToGameOver = true; // Temporary immunity applyDropPhysics(releasedPineapple, 1.5); // Give it a gentle initial push fruits.push(releasedPineapple); if (spatialGrid) { spatialGrid.insertObject(releasedPineapple); } // Reset mergeCounter immediately after deciding to release this pineapple mergeCounter = 0; // Immediately create a new static pineapple for the next cycle var newStaticPineapple = new Fruit(FruitTypes.PINEAPPLE); newStaticPineapple._startX = -newStaticPineapple.width + 250; newStaticPineapple.x = newStaticPineapple._startX; newStaticPineapple.y = GAME_CONSTANTS.PINEAPPLE_START_Y; newStaticPineapple.isStatic = true; // --- AGGRESSIVE VISIBILITY FIX for new static pineapple --- tween.stop(newStaticPineapple); newStaticPineapple.alpha = 1.0; newStaticPineapple.visible = true; if (newStaticPineapple.scale && typeof newStaticPineapple.scale.set === 'function') { newStaticPineapple.scale.set(1.0); } else { newStaticPineapple.scaleX = 1.0; newStaticPineapple.scaleY = 1.0; } if (newStaticPineapple.children && newStaticPineapple.children.length > 0) { for (var c = 0; c < newStaticPineapple.children.length; c++) { tween.stop(newStaticPineapple.children[c]); newStaticPineapple.children[c].alpha = 1.0; newStaticPineapple.children[c].visible = true; if (newStaticPineapple.children[c].scale && typeof newStaticPineapple.children[c].scale.set === "function") { newStaticPineapple.children[c].scale.set(1.0); } else { newStaticPineapple.children[c].scaleX = 1.0; newStaticPineapple.children[c].scaleY = 1.0; } } } pineappleActive = false; game.addChild(newStaticPineapple); pineapple = newStaticPineapple; LK.setTimeout(function () { if (releasedPineapple && fruits.indexOf(releasedPineapple) !== -1) { // Check if it still exists and is in game releasedPineapple.immuneToGameOver = false; // --- AGGRESSIVE VISIBILITY FIX --- if (releasedPineapple) { // Ensure pineapple exists tween.stop(releasedPineapple); // Stop ALL existing tweens on this specific pineapple instance releasedPineapple.alpha = 1.0; releasedPineapple.visible = true; if (releasedPineapple.scale && typeof releasedPineapple.scale.set === 'function') { releasedPineapple.scale.set(1.0); } else { releasedPineapple.scaleX = 1.0; releasedPineapple.scaleY = 1.0; } // Also iterate through direct children (the visual asset) if (releasedPineapple.children && releasedPineapple.children.length > 0) { for (var cIdx = 0; cIdx < releasedPineapple.children.length; cIdx++) { var child = releasedPineapple.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 --- } // No call to setupPineapple() or pineappleActive reset here }, 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(); // No longer handle charged orange release here; handled in handleChargedRelease 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 single source of ellipseEllipseIntersect from global helper 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._startX = -pineapple.width + 250; // Store the fixed starting X position pineapple.x = pineapple._startX; // Set pineapple's X to the starting position pineapple.y = GAME_CONSTANTS.PINEAPPLE_START_Y; pineapple.isStatic = true; // Static until activated // --- AGGRESSIVE VISIBILITY FIX --- // Stop any existing tweens that might be affecting visibility tween.stop(pineapple); 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; } // Ensure children are also visible if applicable if (pineapple.children && pineapple.children.length > 0) { for (var c = 0; c < pineapple.children.length; c++) { // Stop any tweens on child that might affect visibility tween.stop(pineapple.children[c]); pineapple.children[c].alpha = 1.0; pineapple.children[c].visible = true; if (pineapple.children[c].scale && typeof pineapple.children[c].scale.set === "function") { pineapple.children[c].scale.set(1.0); } else { pineapple.children[c].scaleX = 1.0; pineapple.children[c].scaleY = 1.0; } } } // --- END AGGRESSIVE VISIBILITY FIX --- pineappleActive = false; // New static pineapple is not the "active" dynamic one game.addChild(pineapple); } function pushPineapple() { // Always animate the pineapple moving a bit to the right after every merge if (!pineappleActive && pineapple) { // Make sure _startX exists and is valid, otherwise initialize it if (typeof pineapple._startX !== 'number') { pineapple._startX = -pineapple.width + 250; } // Move pineapple a bit to the right for each merge, up to a max position var startX = pineapple._startX; // Always use the stored starting position 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.PINEAPPLE_MERGES_NEEDED; // Use pineapple-specific constant // Use Math.min to clamp mergeCounter to mergesNeeded var progress = Math.min(mergeCounter, mergesNeeded) / mergesNeeded; var newX = startX + progress * (maxX - startX); // 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 // Handle FireElement flicker tween signal if (activeFireElements[i]._flickerTweenSignal && activeFireElements[i]._pendingFlickerTween) { activeFireElements[i].flickerTween = tween(activeFireElements[i], { alpha: activeFireElements[i]._pendingFlickerTween.alpha }, { duration: activeFireElements[i]._pendingFlickerTween.duration, easing: activeFireElements[i]._pendingFlickerTween.easing, onFinish: activeFireElements[i]._pendingFlickerTween.onFinish }); activeFireElements[i]._flickerTweenSignal = false; activeFireElements[i]._pendingFlickerTween = null; } } } // ChargedBallUI tween handling if (chargedBallUI) { if (chargedBallUI._tweenSignal && chargedBallUI._pendingTween) { tween(chargedBallUI.portalAsset, { scaleX: chargedBallUI._pendingTween.scaleX, scaleY: chargedBallUI._pendingTween.scaleY, alpha: chargedBallUI._pendingTween.alpha // Only set rotation if it is defined }.hasOwnProperty('rotation') && typeof chargedBallUI._pendingTween.rotation !== "undefined" ? { rotation: chargedBallUI._pendingTween.rotation } : {}, // This will be merged below { duration: chargedBallUI._pendingTween.duration, easing: chargedBallUI._pendingTween.easing }); chargedBallUI._tweenSignal = false; chargedBallUI._pendingTween = null; } if (chargedBallUI._pulseTweenSignal && chargedBallUI._pendingPulseTween) { tween(chargedBallUI.portalAsset, { scaleX: chargedBallUI._pendingPulseTween.scaleX, scaleY: chargedBallUI._pendingPulseTween.scaleY }, { duration: chargedBallUI._pendingPulseTween.duration, easing: chargedBallUI._pendingPulseTween.easing, onFinish: chargedBallUI._pendingPulseTween.onFinish }); chargedBallUI._pulseTweenSignal = false; chargedBallUI._pendingPulseTween = null; } } // Handle charged orange (Pickle Rick) release if ready handleChargedRelease(); }; // 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) } function handleChargedRelease() { 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 } }
===================================================================
--- original.js
+++ change.js
@@ -1578,38 +1578,70 @@
}
}
// --- END AGGRESSIVE VISIBILITY FIX ---
// Capture THIS specific pineapple instance before any changes
- var currentReleasedPineapple = pineapple;
- currentReleasedPineapple.isStatic = false; // Allow it to fall
- currentReleasedPineapple.immuneToGameOver = true; // Temporary immunity
- applyDropPhysics(currentReleasedPineapple, 1.5); // Give it a gentle initial push
- fruits.push(currentReleasedPineapple);
+ var releasedPineapple = pineapple;
+ releasedPineapple.isStatic = false; // Allow it to fall
+ releasedPineapple.immuneToGameOver = true; // Temporary immunity
+ applyDropPhysics(releasedPineapple, 1.5); // Give it a gentle initial push
+ fruits.push(releasedPineapple);
if (spatialGrid) {
- spatialGrid.insertObject(currentReleasedPineapple);
+ spatialGrid.insertObject(releasedPineapple);
}
// Reset mergeCounter immediately after deciding to release this pineapple
mergeCounter = 0;
+ // Immediately create a new static pineapple for the next cycle
+ var newStaticPineapple = new Fruit(FruitTypes.PINEAPPLE);
+ newStaticPineapple._startX = -newStaticPineapple.width + 250;
+ newStaticPineapple.x = newStaticPineapple._startX;
+ newStaticPineapple.y = GAME_CONSTANTS.PINEAPPLE_START_Y;
+ newStaticPineapple.isStatic = true;
+ // --- AGGRESSIVE VISIBILITY FIX for new static pineapple ---
+ tween.stop(newStaticPineapple);
+ newStaticPineapple.alpha = 1.0;
+ newStaticPineapple.visible = true;
+ if (newStaticPineapple.scale && typeof newStaticPineapple.scale.set === 'function') {
+ newStaticPineapple.scale.set(1.0);
+ } else {
+ newStaticPineapple.scaleX = 1.0;
+ newStaticPineapple.scaleY = 1.0;
+ }
+ if (newStaticPineapple.children && newStaticPineapple.children.length > 0) {
+ for (var c = 0; c < newStaticPineapple.children.length; c++) {
+ tween.stop(newStaticPineapple.children[c]);
+ newStaticPineapple.children[c].alpha = 1.0;
+ newStaticPineapple.children[c].visible = true;
+ if (newStaticPineapple.children[c].scale && typeof newStaticPineapple.children[c].scale.set === "function") {
+ newStaticPineapple.children[c].scale.set(1.0);
+ } else {
+ newStaticPineapple.children[c].scaleX = 1.0;
+ newStaticPineapple.children[c].scaleY = 1.0;
+ }
+ }
+ }
+ pineappleActive = false;
+ game.addChild(newStaticPineapple);
+ pineapple = newStaticPineapple;
LK.setTimeout(function () {
- if (currentReleasedPineapple && fruits.indexOf(currentReleasedPineapple) !== -1) {
+ if (releasedPineapple && fruits.indexOf(releasedPineapple) !== -1) {
// Check if it still exists and is in game
- currentReleasedPineapple.immuneToGameOver = false;
+ releasedPineapple.immuneToGameOver = false;
// --- AGGRESSIVE VISIBILITY FIX ---
- if (currentReleasedPineapple) {
+ if (releasedPineapple) {
// Ensure pineapple exists
- tween.stop(currentReleasedPineapple); // Stop ALL existing tweens on this specific pineapple instance
- currentReleasedPineapple.alpha = 1.0;
- currentReleasedPineapple.visible = true;
- if (currentReleasedPineapple.scale && typeof currentReleasedPineapple.scale.set === 'function') {
- currentReleasedPineapple.scale.set(1.0);
+ tween.stop(releasedPineapple); // Stop ALL existing tweens on this specific pineapple instance
+ releasedPineapple.alpha = 1.0;
+ releasedPineapple.visible = true;
+ if (releasedPineapple.scale && typeof releasedPineapple.scale.set === 'function') {
+ releasedPineapple.scale.set(1.0);
} else {
- currentReleasedPineapple.scaleX = 1.0;
- currentReleasedPineapple.scaleY = 1.0;
+ releasedPineapple.scaleX = 1.0;
+ releasedPineapple.scaleY = 1.0;
}
// Also iterate through direct children (the visual asset)
- if (currentReleasedPineapple.children && currentReleasedPineapple.children.length > 0) {
- for (var cIdx = 0; cIdx < currentReleasedPineapple.children.length; cIdx++) {
- var child = currentReleasedPineapple.children[cIdx];
+ if (releasedPineapple.children && releasedPineapple.children.length > 0) {
+ for (var cIdx = 0; cIdx < releasedPineapple.children.length; cIdx++) {
+ var child = releasedPineapple.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') {
@@ -1622,12 +1654,9 @@
}
}
// --- END AGGRESSIVE VISIBILITY FIX ---
}
- // The system is now ready for a new static pineapple
- pineappleActive = false;
- // Create a new static pineapple for the next cycle
- setupPineapple();
+ // No call to setupPineapple() or pineappleActive reset here
}, GAME_CONSTANTS.PINEAPPLE_IMMUNITY_MS);
}
}
function setupBoundaries() {