Code edit (1 edits merged)
Please save this source code
User prompt
make the gap between the charged icons UI smaller
Code edit (2 edits merged)
Please save this source code
User prompt
make the charged icons smaller
User prompt
move the 9 charging icons more to the left
User prompt
increase the number of charged fruit from 3 to 9. and also change it's mechanics, so instead of charging an icon every merge, it should charge it every time the player drops a fruit.
User prompt
instead of dropping 3 fruits after charge, always drop just 1, regardless of how many fruits have been charged. and release this fruit on a random value on the horizontal position, so it's always dropped in a different position on the board.
Code edit (1 edits merged)
Please save this source code
User prompt
let's change the mechanisms through which the pineapple at the top slides into the board. instead of pushing it when the player releases a fruit, remove that mechanic and replace it with merging. every merge counts towards a push. at the 10th merge, release the pineapple into the board, and reset the mechanism so a new pineapple starts sliding from the 11th merge onwards
User prompt
the trajectory line only updates when I move the finger, but it should also update when fruits bounce around
User prompt
the trajectory line intrerupts too early when touching a fruit. so the line doesnt extend fully to the fruit, it stops a bit too soon. make it to it extends all to the way to the fruit so the last dot actually touches it
User prompt
Add fruit intersection prediction method to TrajectoryLine class for more accurate collision detection
User prompt
✅ Improve hitbox collision detection using oriented bounding boxes for non-circular fruits ✅ Improve wall collision detection to account for fruit rotation ✅ Update right wall collision detection to use oriented bounding box ✅ Improve Floor collision detection with better rotation handling 🔄 Improve Game Over line collision detection for non-circular fruits
User prompt
make the delay 1 second
User prompt
after dropping a fruit, add a 300 miliseconds delay, when you disable clicking, so fruits can;t be released during that interval
User prompt
make the game over line visible again and move it 500 pixels lower
User prompt
move the 3 charging items UI 200 pixels to the right
User prompt
now also increase the gap, or the distance between the fruits that drop from above the screen, so the second fruit remains centered, but the first one is closer to the left edge of the screen while the third one closer to the right egde
Code edit (1 edits merged)
Please save this source code
User prompt
increase the gap between the fruits released from above the screen
User prompt
only have 3 fruits drop instead of 5. update both the UI and the fruits that drop from above the screen
User prompt
make the game over line invisible
User prompt
if I drop a fruit that will merge when it hits the ground, but I then quickly drop another fruit, the previous fruit that will merge, doesn't increase the charge, which punishes me for dropping fruits too fast. allow for a 2 seconds delay before disabling it, so that even if I drop a fruit really fast, the previous fruit has 2 seconds to touch a fruit, to still count as adding a charge
User prompt
only count the player's active merge towards the charging of the fruits. so if the dropped fruit merges with something on the board, increase a charge. further merges don't continue increasing the charges
User prompt
the active fruit can only be level 1 or 2, never 3
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ var Fruit = Container.expand(function (type) { var self = Container.call(this); // FruitTypes is being used before it's defined, so we need to handle this case self.type = type; self.vx = 0; self.vy = 0; self.rotation = 0; self.angularVelocity = 0; self.angularFriction = 0.92; // Increased friction for faster angular velocity reduction self.groundAngularFriction = 0.75; // Stronger ground friction to stop spinning faster self.gravity = 1.8; // Doubled gravity to make fruits drop faster self.friction = 0.98; // Calculate elasticity based on fruit level // The biggest fruit (DURIAN) has elasticity of 0.7 // Smaller fruits are more bouncy with elasticity closer to 1.0 var fruitLevels = { 'CHERRY': 1, 'GRAPE': 2, 'APPLE': 3, 'ORANGE': 4, 'WATERMELON': 5, 'PINEAPPLE': 6, 'MELON': 7, 'PEACH': 8, 'COCONUT': 9, 'DURIAN': 10 }; var currentLevel = self.type ? fruitLevels[self.type.id.toUpperCase()] || 10 : 10; // Scale elasticity from 0.9 (most bouncy) for level 1 to 0.7 (least bouncy) for level 10 self.elasticity = 0.9 - (currentLevel - 1) * (0.2 / 9); self.merging = false; self.isStatic = false; self.maxAngularVelocity = 0.15; // Add maximum angular velocity cap // Only attempt to attach the asset if self.type exists and has necessary properties 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 }); // Set width and height directly from the actual asset for accurate hitbox self.width = fruitGraphics.width; self.height = fruitGraphics.height; // No point value shown on fruit } else { // This will be initialized properly when the game is fully loaded console.log("Warning: Fruit type not available yet or missing required properties"); } self.update = function () { // Skip updates for static or merging fruits if (self.isStatic || self.merging) { return; } // Initialize necessary tracking properties if undefined if (self.safetyPeriod === undefined) { self.safetyPeriod = false; } if (self.wallContactFrames === undefined) { self.wallContactFrames = 0; } // Track safety period state changes if (self.safetyPeriod === false && self.vy <= 0) { // When a fruit that was in safety period starts moving upward or stops, // it means it has hit something, so it's no longer in safety period self.safetyPeriod = true; } // Add damping when velocity is low if (Math.abs(self.vx) < 0.5 && Math.abs(self.vy) < 0.5) { self.angularVelocity *= 0.9; // Apply damping when fruit is almost at rest } // Check for contact with walls to apply wall friction // Use width instead of assuming a radius - better for non-circular fruits var fruitHalfWidth = self.width / 2; var isContactingLeftWall = self.x <= wallLeft.x + wallLeft.width / 2 + fruitHalfWidth + 2; var isContactingRightWall = self.x >= wallRight.x - wallRight.width / 2 - fruitHalfWidth - 2; if (isContactingLeftWall || isContactingRightWall) { // Apply progressive wall friction based on how long the fruit has been in contact self.wallContactFrames++; // Increase wall friction the longer the fruit stays in contact var progressiveFriction = Math.min(0.85, 0.65 + self.wallContactFrames * 0.01); self.angularVelocity *= progressiveFriction; } else { // Reset wall contact frames when not touching walls self.wallContactFrames = 0; } // Check for contact with other fruits to apply additional friction var isContactingOtherFruit = false; for (var i = 0; i < fruits.length; i++) { var otherFruit = fruits[i]; if (otherFruit !== self && !otherFruit.merging && !otherFruit.isStatic) { var dx = otherFruit.x - self.x; var dy = otherFruit.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); var combinedHalfWidths = (self.width + otherFruit.width) / 2; if (distance < combinedHalfWidths + 2) { // Small buffer for contact detection isContactingOtherFruit = true; break; } } } // Apply stronger friction when in contact with other fruits if (isContactingOtherFruit) { self.angularVelocity *= 0.8; // Stronger friction when touching other fruits } // Apply extreme damping when almost stopped rotating if (Math.abs(self.angularVelocity) < 0.01) { self.angularVelocity = 0; } }; self.merge = function (otherFruit) { // Prevent already merging fruits from merging again if (self.merging) { return; } // Mark both fruits as merging to prevent further interactions self.merging = true; otherFruit.merging = true; // Calculate midpoint between fruits for new fruit position var midX = (self.x + otherFruit.x) / 2; var midY = (self.y + otherFruit.y) / 2; // Create merge animation for both fruits tween(self, { alpha: 0, scaleX: 0.5, scaleY: 0.5 }, { duration: 200, easing: tween.easeOut }); tween(otherFruit, { alpha: 0, scaleX: 0.5, scaleY: 0.5 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { // Play merge sound once for all fruit types LK.getSound('merge').play(); // Remove specific merge-based charging logic since all merges charge now // Just marking if the last dropped fruit has merged, for other game mechanics that need it var fromReleasedFruits = self.fromChargedRelease || otherFruit.fromChargedRelease; if (!fromReleasedFruits && (self === lastDroppedFruit || otherFruit === lastDroppedFruit) && !lastDroppedHasMerged) { lastDroppedHasMerged = true; // Mark that this fruit has had its first merge } // Reset fromChargedRelease flag after this merge completes // so these fruits can participate in future chain reactions with next player drop // Special case for DURIAN - they simply disappear instead of merging if (self.type.id.toUpperCase() === 'DURIAN') { // Add points based on the durian's value LK.setScore(LK.getScore() + self.type.points); updateScoreDisplay(); // Remove both fruits removeFruitFromGame(self); removeFruitFromGame(otherFruit); // Any merge charges fruits regardless of circumstance chargeCounter++; updateChargedBallDisplay(); // Check if we've reached 5 charged balls if (chargeCounter >= 5 && !readyToReleaseCharged) { releaseChargedBalls(); } } else { // Any merge charges fruits regardless of circumstance chargeCounter++; updateChargedBallDisplay(); // Check if we've reached 5 charged balls if (chargeCounter >= 5 && !readyToReleaseCharged) { releaseChargedBalls(); } // Normal merge behavior for all other fruits var nextType = FruitTypes[self.type.next.toUpperCase()]; var newFruit = new Fruit(nextType); // Position at midpoint with initial small scale newFruit.x = midX; newFruit.y = midY; newFruit.scaleX = 0.5; newFruit.scaleY = 0.5; // Add to game and array game.addChild(newFruit); fruits.push(newFruit); // Add merge points based on the new fruit's level LK.setScore(LK.getScore() + nextType.points); updateScoreDisplay(); // Animate new fruit growing tween(newFruit, { scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.bounceOut }); // Remove both original fruits removeFruitFromGame(self); removeFruitFromGame(otherFruit); // Removed charged balls check here since it's now handled at the beginning of merge function } } }); // Helper function to remove a fruit from the game function removeFruitFromGame(fruit) { var index = fruits.indexOf(fruit); if (index !== -1) { fruits.splice(index, 1); } fruit.destroy(); } }; return self; }); var Line = Container.expand(function () { var self = Container.call(this); var lineGraphics = self.attachAsset('floor', { anchorX: 0.5, anchorY: 0.5 }); // Make it visually distinct from the floor lineGraphics.tint = 0xff0000; // Ensure full width is used for collision but visual is thin lineGraphics.height = 20; // Make the visual thinner return self; }); var TrajectoryLine = Container.expand(function () { var self = Container.call(this); self.dots = []; self.dotSpacing = 10; // Space between dots in the trajectory (closer dots) self.dotSize = 15; // Size of trajectory dots (larger dots) self.maxDots = 100; // Maximum number of dots to prevent excessive calculations (increased for longer line) // Create dots self.createDots = function () { // Clear existing dots first self.clearDots(); // Create new dots for (var i = 0; i < self.maxDots; i++) { var dot = new Container(); // Create a small white circle using the trajectory dot asset var dotGraphic = dot.attachAsset('trajectoryDot', { anchorX: 0.5, anchorY: 0.5 }); // Make sure the dot is white and appropriately sized dotGraphic.tint = 0xFFFFFF; dot.scaleX = 0.8; // Make dots more visible dot.scaleY = 0.8; // Initially hide the dot dot.visible = false; // Add to container and array self.addChild(dot); self.dots.push(dot); } }; // Clear all dots self.clearDots = function () { for (var i = 0; i < self.dots.length; i++) { if (self.dots[i]) { self.dots[i].destroy(); } } self.dots = []; }; // Update trajectory based on current active fruit position self.updateTrajectory = function (startX, startY) { if (!activeFruit) { return; } // Hide all dots first for (var i = 0; i < self.dots.length; i++) { self.dots[i].visible = false; } // Physics simulation variables var simX = startX; var simY = startY; var simVX = 0; // Starting with no horizontal velocity var simVY = 0; // Starting with no vertical velocity var gravity = 1.8; // Same gravity as in the game // Show dots along predicted path var dotCount = 0; var hitFruit = false; // Create dots in a straight line directly downward var dotY = startY; var dotSpacing = 25; // Smaller spacing between dots for a more continuous line while (dotCount < self.maxDots && !hitFruit) { // Place dot at current position if (dotCount < self.dots.length) { // Place dot directly below the fruit in a straight line self.dots[dotCount].x = startX; self.dots[dotCount].y = dotY; self.dots[dotCount].visible = true; self.dots[dotCount].alpha = 1.0; dotCount++; } // Move to next dot position dotY += dotSpacing; // Check if we've hit the floor var floorCollisionY = gameFloor.y - gameFloor.height / 2 - activeFruit.width / 2; if (dotY > floorCollisionY) { // Stop at the floor break; } // Check if we've hit any fruits var hitFruit = false; for (var j = 0; j < fruits.length; j++) { var fruit = fruits[j]; if (fruit !== activeFruit && !fruit.merging) { var dx = fruit.x - startX; var dy = fruit.y - dotY; var distance = Math.sqrt(dx * dx + dy * dy); var combinedRadius = (activeFruit.width + fruit.width) / 2; if (distance < combinedRadius) { hitFruit = true; break; } } } if (hitFruit) { break; } } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xf6e58d }); /**** * Game Code ****/ // Game variables var gameOverLine; // Game over line var pineapple; // The pineapple that pushes in from left var pineappleActive = false; // Track if pineapple is active in gameplay var pineapplePushCount = 0; // Count how many times pineapple has been pushed var readyToReleaseCharged = false; // Flag to indicate if charged fruits are ready to be released var trajectoryLine; // Line showing trajectory of active fruit var chargedFruitIconScale = 0.6; // Global scale for charged fruit icons var FruitTypes = { CHERRY: { id: 'cherry', size: 150, points: 1, next: 'grape' }, GRAPE: { id: 'grape', size: 200, points: 2, next: 'apple' }, APPLE: { id: 'apple', size: 250, points: 3, next: 'orange' }, ORANGE: { id: 'orange', size: 300, points: 5, next: 'watermelon' }, WATERMELON: { id: 'watermelon', size: 350, points: 8, next: 'pineapple' }, PINEAPPLE: { id: 'pineapple', size: 400, points: 13, next: 'melon' }, MELON: { id: 'melon', size: 450, points: 21, next: 'peach' }, PEACH: { id: 'peach', size: 500, points: 34, next: 'coconut' }, COCONUT: { id: 'coconut', size: 550, points: 55, next: 'durian' }, DURIAN: { id: 'durian', size: 600, points: 89, next: null } }; var gameWidth = 2048; var gameHeight = 2732; var fruits = []; var nextFruitType = null; var activeFruit = null; // The fruit currently controlled by the player var wallLeft, wallRight, gameFloor; var dropPointY = 200; // Y coordinate where new fruits appear var gameOver = false; var scoreText; var isDragging = false; // Flag to check if the player is currently dragging var chargedBalls = []; // Array to hold charged ball icons var chargedBallContainer = null; // Container for charged ball icons var chargeCounter = 0; // Counter to track dropped balls for charging var lastDroppedFruit = null; // Track last fruit dropped by player var lastDroppedHasMerged = false; // Track if last dropped fruit has already had its first merge // Setup game boundaries function setupBoundaries() { // Left wall wallLeft = game.addChild(LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 })); wallLeft.x = 0; wallLeft.y = gameHeight / 2; // Right wall wallRight = game.addChild(LK.getAsset('wall', { anchorX: 0.5, anchorY: 0.5 })); wallRight.x = gameWidth; wallRight.y = gameHeight / 2; // Floor gameFloor = game.addChild(LK.getAsset('floor', { anchorX: 0.5, anchorY: 0.5 })); gameFloor.x = gameWidth / 2; gameFloor.y = gameHeight; // Game over line gameOverLine = game.addChild(new Line()); gameOverLine.x = gameWidth / 2; gameOverLine.y = 50; // Position 200 pixels higher than before gameOverLine.scaleX = 1; // Make it stretch across the entire width of the screen gameOverLine.scaleY = 0.2; // Make it thinner } // Create new next fruit function createNextFruit() { // Determine which fruit to spawn - only level 1 (CHERRY) or level 2 (GRAPE), never level 3+ var fruitProbability = Math.random(); var fruitType; if (fruitProbability < 0.6) { fruitType = FruitTypes.CHERRY; } else { fruitType = FruitTypes.GRAPE; } nextFruitType = fruitType; // Update display // No explicit preview display needed, the next fruit will be the one the player controls // Create the active fruit activeFruit = new Fruit(nextFruitType); // Position at the location of the last dropped fruit if available, otherwise at the top center if (lastDroppedFruit) { activeFruit.x = lastDroppedFruit.x; activeFruit.y = dropPointY + 200; } else { activeFruit.x = gameWidth / 2; activeFruit.y = dropPointY + 200; } // Make it static while the player controls it activeFruit.isStatic = true; game.addChild(activeFruit); // Update trajectory line for the new fruit if (trajectoryLine) { trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y); } } // Drop fruit at specified position function dropFruit() { if (gameOver || !activeFruit) { return; } // Make the active fruit dynamic so it drops activeFruit.isStatic = false; // Use standardized drop mechanics applyDropPhysics(activeFruit, 3.5); // Standard force for normal fruits // Add the fruit to the main fruits array fruits.push(activeFruit); // Mark this as the player's dropped fruit to track its first merge lastDroppedFruit = activeFruit; lastDroppedHasMerged = false; // Hide all trajectory dots when fruit is dropped if (trajectoryLine) { for (var i = 0; i < trajectoryLine.dots.length; i++) { trajectoryLine.dots[i].visible = false; } } // Reset fromChargedRelease flags on all fruits when a new player fruit is dropped // This allows previously released charged fruits to participate in new chain reactions for (var i = 0; i < fruits.length; i++) { if (fruits[i] && fruits[i].fromChargedRelease) { fruits[i].fromChargedRelease = false; } } // Play drop sound LK.getSound('drop').play(); // Check if we have charged balls ready to be released if (readyToReleaseCharged && chargeCounter >= 5) { // Play the PickleRick sound when releasing the charged fruits LK.getSound('pickleRick').play(); // Create and drop 5 level 3 (apple) balls from above the screen for (var i = 0; i < 5; i++) { // Create the actual fruit var apple = new Fruit(FruitTypes.APPLE); // Position each fruit evenly across the screen width, above the visible area var spacing = gameWidth / 6; // Divide by 6 to get 5 spaces between var xPos = spacing * (i + 1); // Position at 1/6, 2/6, 3/6, 4/6, 5/6 of screen width // Place fruits above the screen apple.x = xPos; apple.y = -apple.height; // Make it dynamic so it drops apple.isStatic = false; // Apply standard drop physics - slightly randomize forces for natural effect var forceMultiplier = 3.5 + (Math.random() * 1 - 0.5); applyDropPhysics(apple, forceMultiplier); // Mark this fruit as coming from charged release apple.fromChargedRelease = true; // Add to game and fruits array game.addChild(apple); fruits.push(apple); } // Reset charge counter chargeCounter = 0; // Reset charged balls UI resetChargedBalls(); // Reset the release flag readyToReleaseCharged = false; } // Charge counter is now only incremented on merges // Handle pineapple logic if (pineappleActive) { // Pineapple is ready to drop after pushes pineapple.isStatic = false; // Use standardized drop mechanics with less force for bigger fruit applyDropPhysics(pineapple, 2.5); // Add to fruits array fruits.push(pineapple); // Start 2-second timer to enable game over contact LK.setTimeout(function () { pineapple.immuneToGameOver = false; }, 2000); // Setup a new pineapple for next cycle setupPineapple(); } else { // Push the pineapple further in pushPineapple(); } // Clear active fruit activeFruit = null; // Create the next fruit immediately createNextFruit(); } // Helper function to standardize drop physics for all fruits function applyDropPhysics(fruit, forceMultiplier) { // Add angle variation - random angle between -10 and +10 degrees var angle = (Math.random() * 20 - 10) * (Math.PI / 180); // Convert to radians // Apply velocity based on angle fruit.vx = Math.sin(angle) * forceMultiplier; fruit.vy = Math.abs(Math.cos(angle) * forceMultiplier); // Make sure initial Y velocity is downward // Mark this fruit as in safety period since it's newly dropped fruit.safetyPeriod = false; // Make it immune to game over for a second fruit.immuneToGameOver = true; // Start 1-second timer to enable game over contact LK.setTimeout(function () { if (fruit && fruits.includes(fruit)) { fruit.immuneToGameOver = false; } }, 1000); } // Update score display function updateScoreDisplay() { scoreText.setText(LK.getScore()); } // Setup UI function setupUI() { // Score display scoreText = new Text2("0", { size: 80, fill: 0xFFFFFF }); scoreText.anchor.set(0.5, 0); LK.gui.top.addChild(scoreText); scoreText.y = 30; // Create charged ball grid setupChargedBallDisplay(); } // Create charged ball grid function setupChargedBallDisplay() { // Create container for charged balls chargedBallContainer = new Container(); game.addChild(chargedBallContainer); // Position the container at the top of the screen - move down slightly chargedBallContainer.y = 150; // Use consistent values for UI balls matching the actual game fruit var ballType = FruitTypes.APPLE; // Level 3 ball type // Make icons smaller and decrease gaps between them // Use the global charged fruit icon scale // Position first element and then space them evenly with exactly 200px gaps (increased from 100px) var startX = gameWidth / 2 + 300; // Center the group of 5 icons and move 500px to the right // Create 5 balls in a single row for (var i = 0; i < 5; i++) { // Changed loop to 5 var ball = new Container(); // Use apple as charged balls with consistent scale var ballGraphics = ball.attachAsset('apple', { anchorX: 0.5, anchorY: 0.5 }); // Scale down the apple UI elements for smaller icons ball.scaleX = chargedFruitIconScale; ball.scaleY = chargedFruitIconScale; // Position relative to the first icon with exactly 200px spacing (increased from 100px) ball.x = startX + i * 150; // Set to semi-transparent initially ball.alpha = 0.5; // Add to container and array chargedBallContainer.addChild(ball); chargedBalls.push(ball); } // Center the container horizontally chargedBallContainer.x = 0; } // Function to update charged ball display function updateChargedBallDisplay() { // Update the visibility of balls based on chargeCounter for (var i = 0; i < chargedBalls.length; i++) { if (i < chargeCounter) { // Balls that are charged are fully visible if (chargedBalls[i].alpha !== 1) { tween(chargedBalls[i], { alpha: 1 }, { duration: 300, easing: tween.easeOut }); } } else { // Balls that are not yet charged are semi-transparent if (chargedBalls[i].alpha !== 0.5) { chargedBalls[i].alpha = 0.5; } } } } // Function to prepare charged balls for release (just sets a flag, doesn't release them yet) function releaseChargedBalls() { // Don't play drop sound here anymore as we're not dropping yet // Just set a flag to indicate we have charged balls ready to be released readyToReleaseCharged = true; // Make all charged balls visible to indicate they're charged for (var i = 0; i < chargedBalls.length; i++) { tween(chargedBalls[i], { alpha: 1 }, { duration: 300, easing: tween.easeOut }); } // We don't reset the charge counter or UI here - we'll do that when the charged fruits are actually released } // Separate function to reset charged balls UI function resetChargedBalls() { // Reset the charged ball display immediately for (var j = 0; j < chargedBalls.length; j++) { // Explicitly reset all balls to inactive state with tween to ensure transition tween(chargedBalls[j], { alpha: 0.5 }, { duration: 200, easing: tween.easeOut }); chargedBalls[j].scaleX = chargedFruitIconScale; // Use global scale variable chargedBalls[j].scaleY = chargedFruitIconScale; // Use global scale variable } // No need to call updateChargedBallDisplay() as we set the state directly here } // Check for fruit collisions function checkFruitCollisions() { for (var i = 0; i < fruits.length; i++) { var fruit1 = fruits[i]; // Skip collision for the active fruit if (fruit1 === activeFruit || fruit1.merging) { continue; } for (var j = i + 1; j < fruits.length; j++) { var fruit2 = fruits[j]; // Skip collision for the active fruit if (fruit2 === activeFruit || fruit2.merging) { continue; } // Calculate distance between centers var dx = fruit2.x - fruit1.x; var dy = fruit2.y - fruit1.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if they are overlapping - use actual asset dimensions for more accurate hitboxes // Calculate half dimensions for both fruits var fruit1HalfWidth = fruit1.width / 2; var fruit1HalfHeight = fruit1.height / 2; var fruit2HalfWidth = fruit2.width / 2; var fruit2HalfHeight = fruit2.height / 2; // Check for collision using Rectangle Intersection algorithm // First, calculate the distance between centers on each axis var absDistanceX = Math.abs(dx); var absDistanceY = Math.abs(dy); // Then calculate the sum of half-widths and half-heights var combinedHalfWidths = fruit1HalfWidth + fruit2HalfWidth; var combinedHalfHeights = fruit1HalfHeight + fruit2HalfHeight; // If distance on either axis is less than combined halves, we have an overlap if (absDistanceX < combinedHalfWidths && absDistanceY < combinedHalfHeights) { // Resolve collision (simple separation and velocity adjustment) var combinedRadius = Math.min(combinedHalfWidths, combinedHalfHeights); var overlap = combinedRadius - distance; var normalizeX = dx / distance; var normalizeY = dy / distance; var moveX = overlap / 2 * normalizeX; var moveY = overlap / 2 * normalizeY; fruit1.x -= moveX; fruit1.y -= moveY; fruit2.x += moveX; fruit2.y += moveY; // Calculate relative velocity var rvX = fruit2.vx - fruit1.vx; var rvY = fruit2.vy - fruit1.vy; var contactVelocity = rvX * normalizeX + rvY * normalizeY; // Only resolve if velocities are separating if (contactVelocity < 0) { // Use the higher elasticity for the collision (smaller fruits bounce more) var collisionElasticity = Math.max(fruit1.elasticity, fruit2.elasticity); var impulse = -(1 + collisionElasticity) * contactVelocity; var totalMass = fruit1.type.size + fruit2.type.size; // Using size as a proxy for mass var impulse1 = impulse * (fruit2.type.size / totalMass); var impulse2 = impulse * (fruit1.type.size / totalMass); // Apply impact scaling for smaller fruits against bigger ones // Smaller fruits should bounce away more from larger fruits var sizeDifference = Math.abs(fruit1.type.size - fruit2.type.size) / Math.max(fruit1.type.size, fruit2.type.size); if (fruit1.type.size < fruit2.type.size) { impulse1 *= 1 + sizeDifference * 0.5; // Smaller fruit gets extra bounce } else if (fruit2.type.size < fruit1.type.size) { impulse2 *= 1 + sizeDifference * 0.5; // Smaller fruit gets extra bounce } fruit1.vx -= impulse1 * normalizeX; fruit1.vy -= impulse1 * normalizeY; fruit2.vx += impulse2 * normalizeX; fruit2.vy += impulse2 * normalizeY; // Apply friction between colliding fruits var tangentX = -normalizeY; var tangentY = normalizeX; var tangentVelocity = rvX * tangentX + rvY * tangentY; var frictionImpulse = -tangentVelocity * 0.2; // Increased friction factor fruit1.vx -= frictionImpulse * tangentX; fruit1.vy -= frictionImpulse * tangentY; fruit2.vx += frictionImpulse * tangentX; fruit2.vy += frictionImpulse * tangentY; // Apply angular velocity change based on collision with proportional damping var fruit1AngularImpulse = impulse1 * (tangentX * normalizeY - tangentY * normalizeX) * 0.0005; var fruit2AngularImpulse = impulse2 * (tangentX * normalizeY - tangentY * normalizeX) * 0.0005; fruit1.angularVelocity += fruit1AngularImpulse; fruit2.angularVelocity -= fruit2AngularImpulse; // Apply additional angular damping during collisions fruit1.angularVelocity *= 0.9; fruit2.angularVelocity *= 0.9; // Cap angular velocity fruit1.angularVelocity = Math.min(Math.max(fruit1.angularVelocity, -fruit1.maxAngularVelocity), fruit1.maxAngularVelocity); fruit2.angularVelocity = Math.min(Math.max(fruit2.angularVelocity, -fruit2.maxAngularVelocity), fruit2.maxAngularVelocity); if (Math.abs(contactVelocity) > 1) { // Bounce sound removed } } // Only proceed with merge if fruits are the same type if (fruit1.type === fruit2.type) { // Merge fruits if they are close enough and of the same type fruit1.merge(fruit2); break; } } } } } // Check if game is over (fruits touching the red line) function checkGameOver() { if (gameOver) { return; } // Remove "too many fruits" game over condition // Check if any fruits are touching the red line for (var i = 0; i < fruits.length; i++) { // Don't check game over for the active fruit if (fruits[i] === activeFruit) { continue; } // Check if fruit touches the game over line // For more accurate collision, calculate if any part of the fruit is above the line var fruit = fruits[i]; var fruitHalfHeight = fruit.height / 2; var fruitHalfWidth = fruit.width / 2; // Calculate the effective height based on fruit's rotation var cosAngle = Math.abs(Math.cos(fruit.rotation)); var sinAngle = Math.abs(Math.sin(fruit.rotation)); var effectiveHeight = fruitHalfHeight * cosAngle + fruitHalfWidth * sinAngle; // Check if the top of the fruit is above/at the game over line var fruitTopY = fruit.y - effectiveHeight; var lineBottomY = gameOverLine.y + gameOverLine.height / 2; if (!fruit.merging && fruitTopY <= lineBottomY) { // Skip game over if the fruit is immune (any fruit in grace period) if (fruit.immuneToGameOver) { continue; } // Initialize the safetyPeriod property if not already set if (fruits[i].safetyPeriod === undefined) { // If the fruit is still moving downward, it's probably just spawned if (fruits[i].vy > 0) { // Mark this fruit as in safety period - it has just been dropped fruits[i].safetyPeriod = false; continue; // Skip game over check for freshly dropped fruits } // If the fruit has hit something and bounced back or is stable, it's no longer in safety period if (fruits[i].vy <= 0) { fruits[i].safetyPeriod = true; // Mark that we've checked and it's now unsafe } } // Only trigger game over if the fruit is not in safety period if (fruits[i].safetyPeriod) { // Trigger game over when a fruit touches the line after having bounced/settled gameOver = true; LK.showGameOver(); return; } } } } // Create and setup the pineapple function setupPineapple() { pineapple = new Fruit(FruitTypes.PINEAPPLE); pineapple.x = -pineapple.width / 2; // Start completely off screen pineapple.y = 200; // Position 200 pixels higher than before pineapple.isStatic = true; // Make it static until it's dropped pineappleActive = false; // Not active in gameplay yet pineapplePushCount = 0; // Reset push count game.addChild(pineapple); } // Function to push the pineapple function pushPineapple() { // Only push if not already active in gameplay if (!pineappleActive) { pineapplePushCount++; // Calculate new x position (100px per push instead of 200px) var newX = -pineapple.width / 2 + pineapplePushCount * 70; // Animate the push tween(pineapple, { x: newX }, { duration: 300, easing: tween.bounceOut }); // Check if we've pushed the pineapple 7 times if (pineapplePushCount >= 7) { pineappleActive = true; // Mark as ready to drop on next fruit release } } } // Initialize game function initGame() { LK.setScore(0); gameOver = false; fruits = []; chargeCounter = 0; chargedBalls = []; readyToReleaseCharged = false; lastScoreCheckForCoconut = 0; lastDroppedFruit = null; lastDroppedHasMerged = false; // We no longer reset fromChargedRelease flag here // as we want it to persist only for the current chain reaction // fromChargedRelease is now reset in the merge function when needed // Start background music LK.playMusic('bgmusic'); // Setup game elements setupBoundaries(); setupUI(); setupPineapple(); // Setup the pineapple // Create trajectory line if (trajectoryLine) { trajectoryLine.destroy(); } trajectoryLine = game.addChild(new TrajectoryLine()); trajectoryLine.createDots(); updateScoreDisplay(); createNextFruit(); // Ensure all UI balls are properly initialized to inactive state LK.setTimeout(function () { if (chargedBalls.length === 3) { resetChargedBalls(); } }, 100); } // Function to spawn coconut from bottom of screen function spawnCoconut() { var coconut = new Fruit(FruitTypes.COCONUT); // Randomly position coconut horizontally within game area var minX = wallLeft.x + wallLeft.width / 2 + coconut.width / 2 + 50; var maxX = wallRight.x - wallRight.width / 2 - coconut.width / 2 - 50; coconut.x = minX + Math.random() * (maxX - minX); // Position below the screen coconut.y = gameHeight + coconut.height / 2; // Make it static while animating into place coconut.isStatic = true; // Play the Stonks sound when coconut appears LK.getSound('stonks').play(); // Add to game and fruits array game.addChild(coconut); fruits.push(coconut); // Mark as in safety period coconut.safetyPeriod = false; // Make it immune to game over for a second coconut.immuneToGameOver = true; // Use tween to smoothly animate the coconut entering the screen from below // Calculate target Y position where the coconut is fully in the board var targetY = gameHeight - gameFloor.height / 2 - coconut.height / 2 - 10; // Animate entry with an easeOut effect and gradually increasing speed tween(coconut, { y: targetY }, { duration: 1200, // 1.2 seconds for a faster entry easing: tween.easeIn, // Start slow and speed up onFinish: function onFinish() { // Once fully entered, make it dynamic so it can interact with other fruits coconut.isStatic = false; // Give it a small upward push to make it bounce slightly when it enters coconut.vy = -2; // Add random horizontal velocity for natural movement coconut.vx = (Math.random() * 2 - 1) * 1.5; // Start 1-second timer to enable game over contact LK.setTimeout(function () { if (coconut && fruits.includes(coconut)) { coconut.immuneToGameOver = false; } }, 1000); } }); } // Track last score checked for coconut spawn var lastScoreCheckForCoconut = 0; // Event handlers game.down = function (x, y) { // We don't need to check specific boundaries to start dragging. // As long as there's an active fruit, we can start dragging. if (activeFruit) { isDragging = true; // Update active fruit position immediately game.move(x, y); } }; // Mouse or touch move on game object game.move = function (x, y) { if (isDragging && activeFruit) { // Only move the active fruit on the X axis - use actual fruit width var fruitRadius = activeFruit.width / 2; var minX = wallLeft.x + wallLeft.width / 2 + fruitRadius; var maxX = wallRight.x - wallRight.width / 2 - fruitRadius; activeFruit.x = Math.max(minX, Math.min(maxX, x)); // Update trajectory line if (trajectoryLine) { trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y); } } }; // Mouse or touch up on game object game.up = function () { if (isDragging && activeFruit) { dropFruit(); } isDragging = false; }; // Game update loop game.update = function () { // Check if we've reached a new 500-point threshold var currentScore = LK.getScore(); if (Math.floor(currentScore / 500) > Math.floor(lastScoreCheckForCoconut / 500)) { // Spawn a coconut for every 500 points spawnCoconut(); } lastScoreCheckForCoconut = currentScore; // We no longer need to check if pineapple is in the board // as we now use push count to determine when it's ready // Apply physics and check collisions for each fruit for (var i = fruits.length - 1; i >= 0; i--) { var fruit = fruits[i]; if (fruit.isStatic || fruit.merging) { continue; } // Store last position for boundary checks if (fruit.lastY === undefined) { fruit.lastY = fruit.y; } if (fruit.lastX === undefined) { fruit.lastX = fruit.x; } // Apply gravity fruit.vy += fruit.gravity; // Apply velocity fruit.x += fruit.vx; fruit.y += fruit.vy; // Apply rotation fruit.rotation += fruit.angularVelocity; // Apply friction fruit.vx *= fruit.friction; fruit.vy *= fruit.friction; // Apply angular friction fruit.angularVelocity *= fruit.angularFriction; // Force fruits to stop rotating when they're barely moving if (Math.abs(fruit.vx) < 0.1 && Math.abs(fruit.vy) < 0.1 && Math.abs(fruit.angularVelocity) < 0.03) { fruit.angularVelocity = 0; } // Apply stronger angular friction when moving slowly if (Math.abs(fruit.vx) < 0.8 && Math.abs(fruit.vy) < 0.8) { fruit.angularVelocity *= 0.9; } // Clamp angular velocity fruit.angularVelocity = Math.min(Math.max(fruit.angularVelocity, -fruit.maxAngularVelocity), fruit.maxAngularVelocity); // Wall collision - use actual fruit width for accurate collision var fruitHalfWidth = fruit.width / 2; // Use half width of the asset var fruitHalfHeight = fruit.height / 2; // Use half height of the asset // For non-circular fruits, adjust collision bounds based on asset orientation // Use the larger dimension to ensure no part of the fruit goes through walls var collisionRadius = Math.max(fruitHalfWidth, fruitHalfHeight); if (fruit.x < wallLeft.x + wallLeft.width / 2 + collisionRadius) { fruit.x = wallLeft.x + wallLeft.width / 2 + collisionRadius; fruit.vx = -fruit.vx * fruit.elasticity; // Smaller fruits get more angular velocity from impacts var angularImpactMultiplier = 0.005 * (1 + (0.9 - fruit.elasticity) * 5); fruit.angularVelocity += fruit.vy * angularImpactMultiplier * (fruit.vx / Math.abs(fruit.vx || 1)); // Apply angular velocity based on vertical velocity and direction of impact // Apply wall friction - stronger when in wall contact and proportional to fruit size var wallFriction = 0.65 + (fruit.elasticity - 0.7) * 0.5; // More elastic (smaller) fruits get less wall friction fruit.angularVelocity *= wallFriction; fruit.angularVelocity *= fruit.groundAngularFriction; if (Math.abs(fruit.vx) > 1) { // Bounce sound removed } } else if (fruit.x > wallRight.x - wallRight.width / 2 - collisionRadius) { fruit.x = wallRight.x - wallRight.width / 2 - collisionRadius; fruit.vx = -fruit.vx * fruit.elasticity; // Smaller fruits get more angular velocity from impacts var angularImpactMultiplier = 0.005 * (1 + (0.9 - fruit.elasticity) * 5); fruit.angularVelocity -= fruit.vy * angularImpactMultiplier * (fruit.vx / Math.abs(fruit.vx || 1)); // Apply angular velocity based on vertical velocity and direction of impact // Apply wall friction - stronger when in wall contact and proportional to fruit size var wallFriction = 0.65 + (fruit.elasticity - 0.7) * 0.5; // More elastic (smaller) fruits get less wall friction fruit.angularVelocity *= wallFriction; fruit.angularVelocity *= fruit.groundAngularFriction; if (Math.abs(fruit.vx) > 1) { // Bounce sound removed } } // Floor collision - use cached values for better performance // Calculate the effective height based on fruit's rotation var cosAngle = Math.abs(Math.cos(fruit.rotation)); var sinAngle = Math.abs(Math.sin(fruit.rotation)); var effectiveHeight = fruitHalfHeight * cosAngle + fruitHalfWidth * sinAngle; var floorCollisionY = gameFloor.y - gameFloor.height / 2 - effectiveHeight; // Use the values we already calculated above if (fruit.y > floorCollisionY) { fruit.y = gameFloor.y - gameFloor.height / 2 - effectiveHeight; fruit.vy = -fruit.vy * fruit.elasticity; if (Math.abs(fruit.vx) > 0.5) { // Smaller fruits get more angular velocity from impacts var angularImpactMultiplier = 0.01 * (1 + (0.9 - fruit.elasticity) * 5); fruit.angularVelocity += fruit.vx * angularImpactMultiplier * (fruit.vy / Math.abs(fruit.vy || 1)); // Apply angular velocity based on horizontal velocity and direction of impact } // Smaller fruits should spin longer after impact var angularDamping = fruit.elasticity > 0.85 ? 0.85 : fruit.groundAngularFriction; fruit.angularVelocity *= angularDamping; // Smaller fruits take more time to come to rest var restThreshold = 1 + (fruit.elasticity - 0.7) * 10; if (Math.abs(fruit.vy) < restThreshold) { fruit.vy = 0; } // Angular rest threshold should also scale with elasticity var angularRestThreshold = 0.03 * (1 - (fruit.elasticity - 0.7) * 2); if (Math.abs(fruit.angularVelocity) < angularRestThreshold) { fruit.angularVelocity = 0; } if (Math.abs(fruit.vy) > 1) { // Bounce sound removed } } // Update last positions fruit.lastX = fruit.x; fruit.lastY = fruit.y; } // Check for fruit collisions checkFruitCollisions(); // Check game over conditions checkGameOver(); }; // Initialize the game initGame();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Fruit = Container.expand(function (type) {
var self = Container.call(this);
// FruitTypes is being used before it's defined, so we need to handle this case
self.type = type;
self.vx = 0;
self.vy = 0;
self.rotation = 0;
self.angularVelocity = 0;
self.angularFriction = 0.92; // Increased friction for faster angular velocity reduction
self.groundAngularFriction = 0.75; // Stronger ground friction to stop spinning faster
self.gravity = 1.8; // Doubled gravity to make fruits drop faster
self.friction = 0.98;
// Calculate elasticity based on fruit level
// The biggest fruit (DURIAN) has elasticity of 0.7
// Smaller fruits are more bouncy with elasticity closer to 1.0
var fruitLevels = {
'CHERRY': 1,
'GRAPE': 2,
'APPLE': 3,
'ORANGE': 4,
'WATERMELON': 5,
'PINEAPPLE': 6,
'MELON': 7,
'PEACH': 8,
'COCONUT': 9,
'DURIAN': 10
};
var currentLevel = self.type ? fruitLevels[self.type.id.toUpperCase()] || 10 : 10;
// Scale elasticity from 0.9 (most bouncy) for level 1 to 0.7 (least bouncy) for level 10
self.elasticity = 0.9 - (currentLevel - 1) * (0.2 / 9);
self.merging = false;
self.isStatic = false;
self.maxAngularVelocity = 0.15; // Add maximum angular velocity cap
// Only attempt to attach the asset if self.type exists and has necessary properties
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
});
// Set width and height directly from the actual asset for accurate hitbox
self.width = fruitGraphics.width;
self.height = fruitGraphics.height;
// No point value shown on fruit
} else {
// This will be initialized properly when the game is fully loaded
console.log("Warning: Fruit type not available yet or missing required properties");
}
self.update = function () {
// Skip updates for static or merging fruits
if (self.isStatic || self.merging) {
return;
}
// Initialize necessary tracking properties if undefined
if (self.safetyPeriod === undefined) {
self.safetyPeriod = false;
}
if (self.wallContactFrames === undefined) {
self.wallContactFrames = 0;
}
// Track safety period state changes
if (self.safetyPeriod === false && self.vy <= 0) {
// When a fruit that was in safety period starts moving upward or stops,
// it means it has hit something, so it's no longer in safety period
self.safetyPeriod = true;
}
// Add damping when velocity is low
if (Math.abs(self.vx) < 0.5 && Math.abs(self.vy) < 0.5) {
self.angularVelocity *= 0.9; // Apply damping when fruit is almost at rest
}
// Check for contact with walls to apply wall friction
// Use width instead of assuming a radius - better for non-circular fruits
var fruitHalfWidth = self.width / 2;
var isContactingLeftWall = self.x <= wallLeft.x + wallLeft.width / 2 + fruitHalfWidth + 2;
var isContactingRightWall = self.x >= wallRight.x - wallRight.width / 2 - fruitHalfWidth - 2;
if (isContactingLeftWall || isContactingRightWall) {
// Apply progressive wall friction based on how long the fruit has been in contact
self.wallContactFrames++;
// Increase wall friction the longer the fruit stays in contact
var progressiveFriction = Math.min(0.85, 0.65 + self.wallContactFrames * 0.01);
self.angularVelocity *= progressiveFriction;
} else {
// Reset wall contact frames when not touching walls
self.wallContactFrames = 0;
}
// Check for contact with other fruits to apply additional friction
var isContactingOtherFruit = false;
for (var i = 0; i < fruits.length; i++) {
var otherFruit = fruits[i];
if (otherFruit !== self && !otherFruit.merging && !otherFruit.isStatic) {
var dx = otherFruit.x - self.x;
var dy = otherFruit.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var combinedHalfWidths = (self.width + otherFruit.width) / 2;
if (distance < combinedHalfWidths + 2) {
// Small buffer for contact detection
isContactingOtherFruit = true;
break;
}
}
}
// Apply stronger friction when in contact with other fruits
if (isContactingOtherFruit) {
self.angularVelocity *= 0.8; // Stronger friction when touching other fruits
}
// Apply extreme damping when almost stopped rotating
if (Math.abs(self.angularVelocity) < 0.01) {
self.angularVelocity = 0;
}
};
self.merge = function (otherFruit) {
// Prevent already merging fruits from merging again
if (self.merging) {
return;
}
// Mark both fruits as merging to prevent further interactions
self.merging = true;
otherFruit.merging = true;
// Calculate midpoint between fruits for new fruit position
var midX = (self.x + otherFruit.x) / 2;
var midY = (self.y + otherFruit.y) / 2;
// Create merge animation for both fruits
tween(self, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 200,
easing: tween.easeOut
});
tween(otherFruit, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Play merge sound once for all fruit types
LK.getSound('merge').play();
// Remove specific merge-based charging logic since all merges charge now
// Just marking if the last dropped fruit has merged, for other game mechanics that need it
var fromReleasedFruits = self.fromChargedRelease || otherFruit.fromChargedRelease;
if (!fromReleasedFruits && (self === lastDroppedFruit || otherFruit === lastDroppedFruit) && !lastDroppedHasMerged) {
lastDroppedHasMerged = true; // Mark that this fruit has had its first merge
}
// Reset fromChargedRelease flag after this merge completes
// so these fruits can participate in future chain reactions with next player drop
// Special case for DURIAN - they simply disappear instead of merging
if (self.type.id.toUpperCase() === 'DURIAN') {
// Add points based on the durian's value
LK.setScore(LK.getScore() + self.type.points);
updateScoreDisplay();
// Remove both fruits
removeFruitFromGame(self);
removeFruitFromGame(otherFruit);
// Any merge charges fruits regardless of circumstance
chargeCounter++;
updateChargedBallDisplay();
// Check if we've reached 5 charged balls
if (chargeCounter >= 5 && !readyToReleaseCharged) {
releaseChargedBalls();
}
} else {
// Any merge charges fruits regardless of circumstance
chargeCounter++;
updateChargedBallDisplay();
// Check if we've reached 5 charged balls
if (chargeCounter >= 5 && !readyToReleaseCharged) {
releaseChargedBalls();
}
// Normal merge behavior for all other fruits
var nextType = FruitTypes[self.type.next.toUpperCase()];
var newFruit = new Fruit(nextType);
// Position at midpoint with initial small scale
newFruit.x = midX;
newFruit.y = midY;
newFruit.scaleX = 0.5;
newFruit.scaleY = 0.5;
// Add to game and array
game.addChild(newFruit);
fruits.push(newFruit);
// Add merge points based on the new fruit's level
LK.setScore(LK.getScore() + nextType.points);
updateScoreDisplay();
// Animate new fruit growing
tween(newFruit, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.bounceOut
});
// Remove both original fruits
removeFruitFromGame(self);
removeFruitFromGame(otherFruit);
// Removed charged balls check here since it's now handled at the beginning of merge function
}
}
});
// Helper function to remove a fruit from the game
function removeFruitFromGame(fruit) {
var index = fruits.indexOf(fruit);
if (index !== -1) {
fruits.splice(index, 1);
}
fruit.destroy();
}
};
return self;
});
var Line = Container.expand(function () {
var self = Container.call(this);
var lineGraphics = self.attachAsset('floor', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct from the floor
lineGraphics.tint = 0xff0000;
// Ensure full width is used for collision but visual is thin
lineGraphics.height = 20; // Make the visual thinner
return self;
});
var TrajectoryLine = Container.expand(function () {
var self = Container.call(this);
self.dots = [];
self.dotSpacing = 10; // Space between dots in the trajectory (closer dots)
self.dotSize = 15; // Size of trajectory dots (larger dots)
self.maxDots = 100; // Maximum number of dots to prevent excessive calculations (increased for longer line)
// Create dots
self.createDots = function () {
// Clear existing dots first
self.clearDots();
// Create new dots
for (var i = 0; i < self.maxDots; i++) {
var dot = new Container();
// Create a small white circle using the trajectory dot asset
var dotGraphic = dot.attachAsset('trajectoryDot', {
anchorX: 0.5,
anchorY: 0.5
});
// Make sure the dot is white and appropriately sized
dotGraphic.tint = 0xFFFFFF;
dot.scaleX = 0.8; // Make dots more visible
dot.scaleY = 0.8;
// Initially hide the dot
dot.visible = false;
// Add to container and array
self.addChild(dot);
self.dots.push(dot);
}
};
// Clear all dots
self.clearDots = function () {
for (var i = 0; i < self.dots.length; i++) {
if (self.dots[i]) {
self.dots[i].destroy();
}
}
self.dots = [];
};
// Update trajectory based on current active fruit position
self.updateTrajectory = function (startX, startY) {
if (!activeFruit) {
return;
}
// Hide all dots first
for (var i = 0; i < self.dots.length; i++) {
self.dots[i].visible = false;
}
// Physics simulation variables
var simX = startX;
var simY = startY;
var simVX = 0; // Starting with no horizontal velocity
var simVY = 0; // Starting with no vertical velocity
var gravity = 1.8; // Same gravity as in the game
// Show dots along predicted path
var dotCount = 0;
var hitFruit = false;
// Create dots in a straight line directly downward
var dotY = startY;
var dotSpacing = 25; // Smaller spacing between dots for a more continuous line
while (dotCount < self.maxDots && !hitFruit) {
// Place dot at current position
if (dotCount < self.dots.length) {
// Place dot directly below the fruit in a straight line
self.dots[dotCount].x = startX;
self.dots[dotCount].y = dotY;
self.dots[dotCount].visible = true;
self.dots[dotCount].alpha = 1.0;
dotCount++;
}
// Move to next dot position
dotY += dotSpacing;
// Check if we've hit the floor
var floorCollisionY = gameFloor.y - gameFloor.height / 2 - activeFruit.width / 2;
if (dotY > floorCollisionY) {
// Stop at the floor
break;
}
// Check if we've hit any fruits
var hitFruit = false;
for (var j = 0; j < fruits.length; j++) {
var fruit = fruits[j];
if (fruit !== activeFruit && !fruit.merging) {
var dx = fruit.x - startX;
var dy = fruit.y - dotY;
var distance = Math.sqrt(dx * dx + dy * dy);
var combinedRadius = (activeFruit.width + fruit.width) / 2;
if (distance < combinedRadius) {
hitFruit = true;
break;
}
}
}
if (hitFruit) {
break;
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xf6e58d
});
/****
* Game Code
****/
// Game variables
var gameOverLine; // Game over line
var pineapple; // The pineapple that pushes in from left
var pineappleActive = false; // Track if pineapple is active in gameplay
var pineapplePushCount = 0; // Count how many times pineapple has been pushed
var readyToReleaseCharged = false; // Flag to indicate if charged fruits are ready to be released
var trajectoryLine; // Line showing trajectory of active fruit
var chargedFruitIconScale = 0.6; // Global scale for charged fruit icons
var FruitTypes = {
CHERRY: {
id: 'cherry',
size: 150,
points: 1,
next: 'grape'
},
GRAPE: {
id: 'grape',
size: 200,
points: 2,
next: 'apple'
},
APPLE: {
id: 'apple',
size: 250,
points: 3,
next: 'orange'
},
ORANGE: {
id: 'orange',
size: 300,
points: 5,
next: 'watermelon'
},
WATERMELON: {
id: 'watermelon',
size: 350,
points: 8,
next: 'pineapple'
},
PINEAPPLE: {
id: 'pineapple',
size: 400,
points: 13,
next: 'melon'
},
MELON: {
id: 'melon',
size: 450,
points: 21,
next: 'peach'
},
PEACH: {
id: 'peach',
size: 500,
points: 34,
next: 'coconut'
},
COCONUT: {
id: 'coconut',
size: 550,
points: 55,
next: 'durian'
},
DURIAN: {
id: 'durian',
size: 600,
points: 89,
next: null
}
};
var gameWidth = 2048;
var gameHeight = 2732;
var fruits = [];
var nextFruitType = null;
var activeFruit = null; // The fruit currently controlled by the player
var wallLeft, wallRight, gameFloor;
var dropPointY = 200; // Y coordinate where new fruits appear
var gameOver = false;
var scoreText;
var isDragging = false; // Flag to check if the player is currently dragging
var chargedBalls = []; // Array to hold charged ball icons
var chargedBallContainer = null; // Container for charged ball icons
var chargeCounter = 0; // Counter to track dropped balls for charging
var lastDroppedFruit = null; // Track last fruit dropped by player
var lastDroppedHasMerged = false; // Track if last dropped fruit has already had its first merge
// Setup game boundaries
function setupBoundaries() {
// Left wall
wallLeft = game.addChild(LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
}));
wallLeft.x = 0;
wallLeft.y = gameHeight / 2;
// Right wall
wallRight = game.addChild(LK.getAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
}));
wallRight.x = gameWidth;
wallRight.y = gameHeight / 2;
// Floor
gameFloor = game.addChild(LK.getAsset('floor', {
anchorX: 0.5,
anchorY: 0.5
}));
gameFloor.x = gameWidth / 2;
gameFloor.y = gameHeight;
// Game over line
gameOverLine = game.addChild(new Line());
gameOverLine.x = gameWidth / 2;
gameOverLine.y = 50; // Position 200 pixels higher than before
gameOverLine.scaleX = 1; // Make it stretch across the entire width of the screen
gameOverLine.scaleY = 0.2; // Make it thinner
}
// Create new next fruit
function createNextFruit() {
// Determine which fruit to spawn - only level 1 (CHERRY) or level 2 (GRAPE), never level 3+
var fruitProbability = Math.random();
var fruitType;
if (fruitProbability < 0.6) {
fruitType = FruitTypes.CHERRY;
} else {
fruitType = FruitTypes.GRAPE;
}
nextFruitType = fruitType;
// Update display
// No explicit preview display needed, the next fruit will be the one the player controls
// Create the active fruit
activeFruit = new Fruit(nextFruitType);
// Position at the location of the last dropped fruit if available, otherwise at the top center
if (lastDroppedFruit) {
activeFruit.x = lastDroppedFruit.x;
activeFruit.y = dropPointY + 200;
} else {
activeFruit.x = gameWidth / 2;
activeFruit.y = dropPointY + 200;
}
// Make it static while the player controls it
activeFruit.isStatic = true;
game.addChild(activeFruit);
// Update trajectory line for the new fruit
if (trajectoryLine) {
trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y);
}
}
// Drop fruit at specified position
function dropFruit() {
if (gameOver || !activeFruit) {
return;
}
// Make the active fruit dynamic so it drops
activeFruit.isStatic = false;
// Use standardized drop mechanics
applyDropPhysics(activeFruit, 3.5); // Standard force for normal fruits
// Add the fruit to the main fruits array
fruits.push(activeFruit);
// Mark this as the player's dropped fruit to track its first merge
lastDroppedFruit = activeFruit;
lastDroppedHasMerged = false;
// Hide all trajectory dots when fruit is dropped
if (trajectoryLine) {
for (var i = 0; i < trajectoryLine.dots.length; i++) {
trajectoryLine.dots[i].visible = false;
}
}
// Reset fromChargedRelease flags on all fruits when a new player fruit is dropped
// This allows previously released charged fruits to participate in new chain reactions
for (var i = 0; i < fruits.length; i++) {
if (fruits[i] && fruits[i].fromChargedRelease) {
fruits[i].fromChargedRelease = false;
}
}
// Play drop sound
LK.getSound('drop').play();
// Check if we have charged balls ready to be released
if (readyToReleaseCharged && chargeCounter >= 5) {
// Play the PickleRick sound when releasing the charged fruits
LK.getSound('pickleRick').play();
// Create and drop 5 level 3 (apple) balls from above the screen
for (var i = 0; i < 5; i++) {
// Create the actual fruit
var apple = new Fruit(FruitTypes.APPLE);
// Position each fruit evenly across the screen width, above the visible area
var spacing = gameWidth / 6; // Divide by 6 to get 5 spaces between
var xPos = spacing * (i + 1); // Position at 1/6, 2/6, 3/6, 4/6, 5/6 of screen width
// Place fruits above the screen
apple.x = xPos;
apple.y = -apple.height;
// Make it dynamic so it drops
apple.isStatic = false;
// Apply standard drop physics - slightly randomize forces for natural effect
var forceMultiplier = 3.5 + (Math.random() * 1 - 0.5);
applyDropPhysics(apple, forceMultiplier);
// Mark this fruit as coming from charged release
apple.fromChargedRelease = true;
// Add to game and fruits array
game.addChild(apple);
fruits.push(apple);
}
// Reset charge counter
chargeCounter = 0;
// Reset charged balls UI
resetChargedBalls();
// Reset the release flag
readyToReleaseCharged = false;
}
// Charge counter is now only incremented on merges
// Handle pineapple logic
if (pineappleActive) {
// Pineapple is ready to drop after pushes
pineapple.isStatic = false;
// Use standardized drop mechanics with less force for bigger fruit
applyDropPhysics(pineapple, 2.5);
// Add to fruits array
fruits.push(pineapple);
// Start 2-second timer to enable game over contact
LK.setTimeout(function () {
pineapple.immuneToGameOver = false;
}, 2000);
// Setup a new pineapple for next cycle
setupPineapple();
} else {
// Push the pineapple further in
pushPineapple();
}
// Clear active fruit
activeFruit = null;
// Create the next fruit immediately
createNextFruit();
}
// Helper function to standardize drop physics for all fruits
function applyDropPhysics(fruit, forceMultiplier) {
// Add angle variation - random angle between -10 and +10 degrees
var angle = (Math.random() * 20 - 10) * (Math.PI / 180); // Convert to radians
// Apply velocity based on angle
fruit.vx = Math.sin(angle) * forceMultiplier;
fruit.vy = Math.abs(Math.cos(angle) * forceMultiplier); // Make sure initial Y velocity is downward
// Mark this fruit as in safety period since it's newly dropped
fruit.safetyPeriod = false;
// Make it immune to game over for a second
fruit.immuneToGameOver = true;
// Start 1-second timer to enable game over contact
LK.setTimeout(function () {
if (fruit && fruits.includes(fruit)) {
fruit.immuneToGameOver = false;
}
}, 1000);
}
// Update score display
function updateScoreDisplay() {
scoreText.setText(LK.getScore());
}
// Setup UI
function setupUI() {
// Score display
scoreText = new Text2("0", {
size: 80,
fill: 0xFFFFFF
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
scoreText.y = 30;
// Create charged ball grid
setupChargedBallDisplay();
}
// Create charged ball grid
function setupChargedBallDisplay() {
// Create container for charged balls
chargedBallContainer = new Container();
game.addChild(chargedBallContainer);
// Position the container at the top of the screen - move down slightly
chargedBallContainer.y = 150;
// Use consistent values for UI balls matching the actual game fruit
var ballType = FruitTypes.APPLE; // Level 3 ball type
// Make icons smaller and decrease gaps between them
// Use the global charged fruit icon scale
// Position first element and then space them evenly with exactly 200px gaps (increased from 100px)
var startX = gameWidth / 2 + 300; // Center the group of 5 icons and move 500px to the right
// Create 5 balls in a single row
for (var i = 0; i < 5; i++) {
// Changed loop to 5
var ball = new Container();
// Use apple as charged balls with consistent scale
var ballGraphics = ball.attachAsset('apple', {
anchorX: 0.5,
anchorY: 0.5
});
// Scale down the apple UI elements for smaller icons
ball.scaleX = chargedFruitIconScale;
ball.scaleY = chargedFruitIconScale;
// Position relative to the first icon with exactly 200px spacing (increased from 100px)
ball.x = startX + i * 150;
// Set to semi-transparent initially
ball.alpha = 0.5;
// Add to container and array
chargedBallContainer.addChild(ball);
chargedBalls.push(ball);
}
// Center the container horizontally
chargedBallContainer.x = 0;
}
// Function to update charged ball display
function updateChargedBallDisplay() {
// Update the visibility of balls based on chargeCounter
for (var i = 0; i < chargedBalls.length; i++) {
if (i < chargeCounter) {
// Balls that are charged are fully visible
if (chargedBalls[i].alpha !== 1) {
tween(chargedBalls[i], {
alpha: 1
}, {
duration: 300,
easing: tween.easeOut
});
}
} else {
// Balls that are not yet charged are semi-transparent
if (chargedBalls[i].alpha !== 0.5) {
chargedBalls[i].alpha = 0.5;
}
}
}
}
// Function to prepare charged balls for release (just sets a flag, doesn't release them yet)
function releaseChargedBalls() {
// Don't play drop sound here anymore as we're not dropping yet
// Just set a flag to indicate we have charged balls ready to be released
readyToReleaseCharged = true;
// Make all charged balls visible to indicate they're charged
for (var i = 0; i < chargedBalls.length; i++) {
tween(chargedBalls[i], {
alpha: 1
}, {
duration: 300,
easing: tween.easeOut
});
}
// We don't reset the charge counter or UI here - we'll do that when the charged fruits are actually released
}
// Separate function to reset charged balls UI
function resetChargedBalls() {
// Reset the charged ball display immediately
for (var j = 0; j < chargedBalls.length; j++) {
// Explicitly reset all balls to inactive state with tween to ensure transition
tween(chargedBalls[j], {
alpha: 0.5
}, {
duration: 200,
easing: tween.easeOut
});
chargedBalls[j].scaleX = chargedFruitIconScale; // Use global scale variable
chargedBalls[j].scaleY = chargedFruitIconScale; // Use global scale variable
}
// No need to call updateChargedBallDisplay() as we set the state directly here
}
// Check for fruit collisions
function checkFruitCollisions() {
for (var i = 0; i < fruits.length; i++) {
var fruit1 = fruits[i];
// Skip collision for the active fruit
if (fruit1 === activeFruit || fruit1.merging) {
continue;
}
for (var j = i + 1; j < fruits.length; j++) {
var fruit2 = fruits[j];
// Skip collision for the active fruit
if (fruit2 === activeFruit || fruit2.merging) {
continue;
}
// Calculate distance between centers
var dx = fruit2.x - fruit1.x;
var dy = fruit2.y - fruit1.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if they are overlapping - use actual asset dimensions for more accurate hitboxes
// Calculate half dimensions for both fruits
var fruit1HalfWidth = fruit1.width / 2;
var fruit1HalfHeight = fruit1.height / 2;
var fruit2HalfWidth = fruit2.width / 2;
var fruit2HalfHeight = fruit2.height / 2;
// Check for collision using Rectangle Intersection algorithm
// First, calculate the distance between centers on each axis
var absDistanceX = Math.abs(dx);
var absDistanceY = Math.abs(dy);
// Then calculate the sum of half-widths and half-heights
var combinedHalfWidths = fruit1HalfWidth + fruit2HalfWidth;
var combinedHalfHeights = fruit1HalfHeight + fruit2HalfHeight;
// If distance on either axis is less than combined halves, we have an overlap
if (absDistanceX < combinedHalfWidths && absDistanceY < combinedHalfHeights) {
// Resolve collision (simple separation and velocity adjustment)
var combinedRadius = Math.min(combinedHalfWidths, combinedHalfHeights);
var overlap = combinedRadius - distance;
var normalizeX = dx / distance;
var normalizeY = dy / distance;
var moveX = overlap / 2 * normalizeX;
var moveY = overlap / 2 * normalizeY;
fruit1.x -= moveX;
fruit1.y -= moveY;
fruit2.x += moveX;
fruit2.y += moveY;
// Calculate relative velocity
var rvX = fruit2.vx - fruit1.vx;
var rvY = fruit2.vy - fruit1.vy;
var contactVelocity = rvX * normalizeX + rvY * normalizeY;
// Only resolve if velocities are separating
if (contactVelocity < 0) {
// Use the higher elasticity for the collision (smaller fruits bounce more)
var collisionElasticity = Math.max(fruit1.elasticity, fruit2.elasticity);
var impulse = -(1 + collisionElasticity) * contactVelocity;
var totalMass = fruit1.type.size + fruit2.type.size; // Using size as a proxy for mass
var impulse1 = impulse * (fruit2.type.size / totalMass);
var impulse2 = impulse * (fruit1.type.size / totalMass);
// Apply impact scaling for smaller fruits against bigger ones
// Smaller fruits should bounce away more from larger fruits
var sizeDifference = Math.abs(fruit1.type.size - fruit2.type.size) / Math.max(fruit1.type.size, fruit2.type.size);
if (fruit1.type.size < fruit2.type.size) {
impulse1 *= 1 + sizeDifference * 0.5; // Smaller fruit gets extra bounce
} else if (fruit2.type.size < fruit1.type.size) {
impulse2 *= 1 + sizeDifference * 0.5; // Smaller fruit gets extra bounce
}
fruit1.vx -= impulse1 * normalizeX;
fruit1.vy -= impulse1 * normalizeY;
fruit2.vx += impulse2 * normalizeX;
fruit2.vy += impulse2 * normalizeY;
// Apply friction between colliding fruits
var tangentX = -normalizeY;
var tangentY = normalizeX;
var tangentVelocity = rvX * tangentX + rvY * tangentY;
var frictionImpulse = -tangentVelocity * 0.2; // Increased friction factor
fruit1.vx -= frictionImpulse * tangentX;
fruit1.vy -= frictionImpulse * tangentY;
fruit2.vx += frictionImpulse * tangentX;
fruit2.vy += frictionImpulse * tangentY;
// Apply angular velocity change based on collision with proportional damping
var fruit1AngularImpulse = impulse1 * (tangentX * normalizeY - tangentY * normalizeX) * 0.0005;
var fruit2AngularImpulse = impulse2 * (tangentX * normalizeY - tangentY * normalizeX) * 0.0005;
fruit1.angularVelocity += fruit1AngularImpulse;
fruit2.angularVelocity -= fruit2AngularImpulse;
// Apply additional angular damping during collisions
fruit1.angularVelocity *= 0.9;
fruit2.angularVelocity *= 0.9;
// Cap angular velocity
fruit1.angularVelocity = Math.min(Math.max(fruit1.angularVelocity, -fruit1.maxAngularVelocity), fruit1.maxAngularVelocity);
fruit2.angularVelocity = Math.min(Math.max(fruit2.angularVelocity, -fruit2.maxAngularVelocity), fruit2.maxAngularVelocity);
if (Math.abs(contactVelocity) > 1) {
// Bounce sound removed
}
}
// Only proceed with merge if fruits are the same type
if (fruit1.type === fruit2.type) {
// Merge fruits if they are close enough and of the same type
fruit1.merge(fruit2);
break;
}
}
}
}
}
// Check if game is over (fruits touching the red line)
function checkGameOver() {
if (gameOver) {
return;
}
// Remove "too many fruits" game over condition
// Check if any fruits are touching the red line
for (var i = 0; i < fruits.length; i++) {
// Don't check game over for the active fruit
if (fruits[i] === activeFruit) {
continue;
}
// Check if fruit touches the game over line
// For more accurate collision, calculate if any part of the fruit is above the line
var fruit = fruits[i];
var fruitHalfHeight = fruit.height / 2;
var fruitHalfWidth = fruit.width / 2;
// Calculate the effective height based on fruit's rotation
var cosAngle = Math.abs(Math.cos(fruit.rotation));
var sinAngle = Math.abs(Math.sin(fruit.rotation));
var effectiveHeight = fruitHalfHeight * cosAngle + fruitHalfWidth * sinAngle;
// Check if the top of the fruit is above/at the game over line
var fruitTopY = fruit.y - effectiveHeight;
var lineBottomY = gameOverLine.y + gameOverLine.height / 2;
if (!fruit.merging && fruitTopY <= lineBottomY) {
// Skip game over if the fruit is immune (any fruit in grace period)
if (fruit.immuneToGameOver) {
continue;
}
// Initialize the safetyPeriod property if not already set
if (fruits[i].safetyPeriod === undefined) {
// If the fruit is still moving downward, it's probably just spawned
if (fruits[i].vy > 0) {
// Mark this fruit as in safety period - it has just been dropped
fruits[i].safetyPeriod = false;
continue; // Skip game over check for freshly dropped fruits
}
// If the fruit has hit something and bounced back or is stable, it's no longer in safety period
if (fruits[i].vy <= 0) {
fruits[i].safetyPeriod = true; // Mark that we've checked and it's now unsafe
}
}
// Only trigger game over if the fruit is not in safety period
if (fruits[i].safetyPeriod) {
// Trigger game over when a fruit touches the line after having bounced/settled
gameOver = true;
LK.showGameOver();
return;
}
}
}
}
// Create and setup the pineapple
function setupPineapple() {
pineapple = new Fruit(FruitTypes.PINEAPPLE);
pineapple.x = -pineapple.width / 2; // Start completely off screen
pineapple.y = 200; // Position 200 pixels higher than before
pineapple.isStatic = true; // Make it static until it's dropped
pineappleActive = false; // Not active in gameplay yet
pineapplePushCount = 0; // Reset push count
game.addChild(pineapple);
}
// Function to push the pineapple
function pushPineapple() {
// Only push if not already active in gameplay
if (!pineappleActive) {
pineapplePushCount++;
// Calculate new x position (100px per push instead of 200px)
var newX = -pineapple.width / 2 + pineapplePushCount * 70;
// Animate the push
tween(pineapple, {
x: newX
}, {
duration: 300,
easing: tween.bounceOut
});
// Check if we've pushed the pineapple 7 times
if (pineapplePushCount >= 7) {
pineappleActive = true; // Mark as ready to drop on next fruit release
}
}
}
// Initialize game
function initGame() {
LK.setScore(0);
gameOver = false;
fruits = [];
chargeCounter = 0;
chargedBalls = [];
readyToReleaseCharged = false;
lastScoreCheckForCoconut = 0;
lastDroppedFruit = null;
lastDroppedHasMerged = false;
// We no longer reset fromChargedRelease flag here
// as we want it to persist only for the current chain reaction
// fromChargedRelease is now reset in the merge function when needed
// Start background music
LK.playMusic('bgmusic');
// Setup game elements
setupBoundaries();
setupUI();
setupPineapple(); // Setup the pineapple
// Create trajectory line
if (trajectoryLine) {
trajectoryLine.destroy();
}
trajectoryLine = game.addChild(new TrajectoryLine());
trajectoryLine.createDots();
updateScoreDisplay();
createNextFruit();
// Ensure all UI balls are properly initialized to inactive state
LK.setTimeout(function () {
if (chargedBalls.length === 3) {
resetChargedBalls();
}
}, 100);
}
// Function to spawn coconut from bottom of screen
function spawnCoconut() {
var coconut = new Fruit(FruitTypes.COCONUT);
// Randomly position coconut horizontally within game area
var minX = wallLeft.x + wallLeft.width / 2 + coconut.width / 2 + 50;
var maxX = wallRight.x - wallRight.width / 2 - coconut.width / 2 - 50;
coconut.x = minX + Math.random() * (maxX - minX);
// Position below the screen
coconut.y = gameHeight + coconut.height / 2;
// Make it static while animating into place
coconut.isStatic = true;
// Play the Stonks sound when coconut appears
LK.getSound('stonks').play();
// Add to game and fruits array
game.addChild(coconut);
fruits.push(coconut);
// Mark as in safety period
coconut.safetyPeriod = false;
// Make it immune to game over for a second
coconut.immuneToGameOver = true;
// Use tween to smoothly animate the coconut entering the screen from below
// Calculate target Y position where the coconut is fully in the board
var targetY = gameHeight - gameFloor.height / 2 - coconut.height / 2 - 10;
// Animate entry with an easeOut effect and gradually increasing speed
tween(coconut, {
y: targetY
}, {
duration: 1200,
// 1.2 seconds for a faster entry
easing: tween.easeIn,
// Start slow and speed up
onFinish: function onFinish() {
// Once fully entered, make it dynamic so it can interact with other fruits
coconut.isStatic = false;
// Give it a small upward push to make it bounce slightly when it enters
coconut.vy = -2;
// Add random horizontal velocity for natural movement
coconut.vx = (Math.random() * 2 - 1) * 1.5;
// Start 1-second timer to enable game over contact
LK.setTimeout(function () {
if (coconut && fruits.includes(coconut)) {
coconut.immuneToGameOver = false;
}
}, 1000);
}
});
}
// Track last score checked for coconut spawn
var lastScoreCheckForCoconut = 0;
// Event handlers
game.down = function (x, y) {
// We don't need to check specific boundaries to start dragging.
// As long as there's an active fruit, we can start dragging.
if (activeFruit) {
isDragging = true;
// Update active fruit position immediately
game.move(x, y);
}
};
// Mouse or touch move on game object
game.move = function (x, y) {
if (isDragging && activeFruit) {
// Only move the active fruit on the X axis - use actual fruit width
var fruitRadius = activeFruit.width / 2;
var minX = wallLeft.x + wallLeft.width / 2 + fruitRadius;
var maxX = wallRight.x - wallRight.width / 2 - fruitRadius;
activeFruit.x = Math.max(minX, Math.min(maxX, x));
// Update trajectory line
if (trajectoryLine) {
trajectoryLine.updateTrajectory(activeFruit.x, activeFruit.y);
}
}
};
// Mouse or touch up on game object
game.up = function () {
if (isDragging && activeFruit) {
dropFruit();
}
isDragging = false;
};
// Game update loop
game.update = function () {
// Check if we've reached a new 500-point threshold
var currentScore = LK.getScore();
if (Math.floor(currentScore / 500) > Math.floor(lastScoreCheckForCoconut / 500)) {
// Spawn a coconut for every 500 points
spawnCoconut();
}
lastScoreCheckForCoconut = currentScore;
// We no longer need to check if pineapple is in the board
// as we now use push count to determine when it's ready
// Apply physics and check collisions for each fruit
for (var i = fruits.length - 1; i >= 0; i--) {
var fruit = fruits[i];
if (fruit.isStatic || fruit.merging) {
continue;
}
// Store last position for boundary checks
if (fruit.lastY === undefined) {
fruit.lastY = fruit.y;
}
if (fruit.lastX === undefined) {
fruit.lastX = fruit.x;
}
// Apply gravity
fruit.vy += fruit.gravity;
// Apply velocity
fruit.x += fruit.vx;
fruit.y += fruit.vy;
// Apply rotation
fruit.rotation += fruit.angularVelocity;
// Apply friction
fruit.vx *= fruit.friction;
fruit.vy *= fruit.friction;
// Apply angular friction
fruit.angularVelocity *= fruit.angularFriction;
// Force fruits to stop rotating when they're barely moving
if (Math.abs(fruit.vx) < 0.1 && Math.abs(fruit.vy) < 0.1 && Math.abs(fruit.angularVelocity) < 0.03) {
fruit.angularVelocity = 0;
}
// Apply stronger angular friction when moving slowly
if (Math.abs(fruit.vx) < 0.8 && Math.abs(fruit.vy) < 0.8) {
fruit.angularVelocity *= 0.9;
}
// Clamp angular velocity
fruit.angularVelocity = Math.min(Math.max(fruit.angularVelocity, -fruit.maxAngularVelocity), fruit.maxAngularVelocity);
// Wall collision - use actual fruit width for accurate collision
var fruitHalfWidth = fruit.width / 2; // Use half width of the asset
var fruitHalfHeight = fruit.height / 2; // Use half height of the asset
// For non-circular fruits, adjust collision bounds based on asset orientation
// Use the larger dimension to ensure no part of the fruit goes through walls
var collisionRadius = Math.max(fruitHalfWidth, fruitHalfHeight);
if (fruit.x < wallLeft.x + wallLeft.width / 2 + collisionRadius) {
fruit.x = wallLeft.x + wallLeft.width / 2 + collisionRadius;
fruit.vx = -fruit.vx * fruit.elasticity;
// Smaller fruits get more angular velocity from impacts
var angularImpactMultiplier = 0.005 * (1 + (0.9 - fruit.elasticity) * 5);
fruit.angularVelocity += fruit.vy * angularImpactMultiplier * (fruit.vx / Math.abs(fruit.vx || 1)); // Apply angular velocity based on vertical velocity and direction of impact
// Apply wall friction - stronger when in wall contact and proportional to fruit size
var wallFriction = 0.65 + (fruit.elasticity - 0.7) * 0.5; // More elastic (smaller) fruits get less wall friction
fruit.angularVelocity *= wallFriction;
fruit.angularVelocity *= fruit.groundAngularFriction;
if (Math.abs(fruit.vx) > 1) {
// Bounce sound removed
}
} else if (fruit.x > wallRight.x - wallRight.width / 2 - collisionRadius) {
fruit.x = wallRight.x - wallRight.width / 2 - collisionRadius;
fruit.vx = -fruit.vx * fruit.elasticity;
// Smaller fruits get more angular velocity from impacts
var angularImpactMultiplier = 0.005 * (1 + (0.9 - fruit.elasticity) * 5);
fruit.angularVelocity -= fruit.vy * angularImpactMultiplier * (fruit.vx / Math.abs(fruit.vx || 1)); // Apply angular velocity based on vertical velocity and direction of impact
// Apply wall friction - stronger when in wall contact and proportional to fruit size
var wallFriction = 0.65 + (fruit.elasticity - 0.7) * 0.5; // More elastic (smaller) fruits get less wall friction
fruit.angularVelocity *= wallFriction;
fruit.angularVelocity *= fruit.groundAngularFriction;
if (Math.abs(fruit.vx) > 1) {
// Bounce sound removed
}
}
// Floor collision - use cached values for better performance
// Calculate the effective height based on fruit's rotation
var cosAngle = Math.abs(Math.cos(fruit.rotation));
var sinAngle = Math.abs(Math.sin(fruit.rotation));
var effectiveHeight = fruitHalfHeight * cosAngle + fruitHalfWidth * sinAngle;
var floorCollisionY = gameFloor.y - gameFloor.height / 2 - effectiveHeight;
// Use the values we already calculated above
if (fruit.y > floorCollisionY) {
fruit.y = gameFloor.y - gameFloor.height / 2 - effectiveHeight;
fruit.vy = -fruit.vy * fruit.elasticity;
if (Math.abs(fruit.vx) > 0.5) {
// Smaller fruits get more angular velocity from impacts
var angularImpactMultiplier = 0.01 * (1 + (0.9 - fruit.elasticity) * 5);
fruit.angularVelocity += fruit.vx * angularImpactMultiplier * (fruit.vy / Math.abs(fruit.vy || 1)); // Apply angular velocity based on horizontal velocity and direction of impact
}
// Smaller fruits should spin longer after impact
var angularDamping = fruit.elasticity > 0.85 ? 0.85 : fruit.groundAngularFriction;
fruit.angularVelocity *= angularDamping;
// Smaller fruits take more time to come to rest
var restThreshold = 1 + (fruit.elasticity - 0.7) * 10;
if (Math.abs(fruit.vy) < restThreshold) {
fruit.vy = 0;
}
// Angular rest threshold should also scale with elasticity
var angularRestThreshold = 0.03 * (1 - (fruit.elasticity - 0.7) * 2);
if (Math.abs(fruit.angularVelocity) < angularRestThreshold) {
fruit.angularVelocity = 0;
}
if (Math.abs(fruit.vy) > 1) {
// Bounce sound removed
}
}
// Update last positions
fruit.lastX = fruit.x;
fruit.lastY = fruit.y;
}
// Check for fruit collisions
checkFruitCollisions();
// Check game over conditions
checkGameOver();
};
// Initialize the game
initGame();