User prompt
remove the comment line shake button script. We need to see this button It need to work shown on the screenuฬ โช๐ก Consider importing and using the following plugins: @upit/tween.v1
User prompt
add a button to lower screen. It shake the bottle left and right to better feeling โช๐ก Consider importing and using the following plugins: @upit/tween.v1
User prompt
when colliders inside eacouther. Make a better push each other. Better powerful push. Reduce gravity to prevent shaking
User prompt
fix hold and drop system
User prompt
fix gravity move shaking. Fix left mouse click hold system. When hold its need to drop and drop and drop drop drop drop โช๐ก Consider importing and using the following plugins: @upit/tween.v1
User prompt
improve collider fruits become inside and inside eachothers. + When I hold left mouse click it drop next and next fuirt like machine gun. โช๐ก Consider importing and using the following plugins: @upit/tween.v1
User prompt
1) fix colliders. when merge happend it need to push other fruits like explode 2) add particles when merge 3) when I hold left click drop and drop even I release + I can click to drop 4) fix gravity. Its shaking when stacking 2 or 3 fruit up and up โช๐ก Consider importing and using the following plugins: @upit/tween.v1
User prompt
1) dropped furit dont reackt the counter. After 3 second countdown start for that new fruit. 2) fix hitbox colliders sometimes fruits going inside eachothers
User prompt
The bigger dropable fruit is fruit_grape. Next fruit is visible on right top corner.
User prompt
the countdown count every second. 3 (1 sec later) 2 .....
User prompt
its insta gameover now. Check the code and fix that
User prompt
fix the game over state. Add 3 2 1 countdown
User prompt
current fruit follow the mose on x axis. when I click the left mouse button it must drop on the glass
User prompt
okay do like that. Add a line to furit. This line indicate the fruits drop spot
Code edit (1 edits merged)
Please save this source code
User prompt
Suika Merge Fruits
Initial prompt
make me a suika game clon
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Fruit class var Fruit = Container.expand(function () { var self = Container.call(this); // Properties self.fruitType = 0; // index in FRUIT_LIST self.radius = 60; // default, will be set below self.vx = 0; self.vy = 0; self.isDropping = false; // true if falling, false if held at top self.isMerging = false; // true if currently merging (to avoid double merges) self.lastMergedTick = -1; // to prevent double merges in one tick // Attach fruit asset self.setType = function (typeIdx) { self.fruitType = typeIdx; if (self.fruitAsset) { self.removeChild(self.fruitAsset); } var fruitId = FRUIT_LIST[typeIdx].id; self.fruitAsset = self.attachAsset(fruitId, { anchorX: 0.5, anchorY: 0.5 }); self.radius = self.fruitAsset.width / 2; }; // Set initial type self.setType(0); // Set position self.setPosition = function (x, y) { self.x = x; self.y = y; }; // Physics update self.update = function () { if (!self.isDropping) return; // Gravity self.vy += 0.62; // reduced gravity for more stable stacking and less shake // Clamp vy if (self.vy > 16) self.vy = 16; // Move self.x += self.vx; self.y += self.vy; // Dampen tiny velocities to avoid jitter if (Math.abs(self.vx) < 0.03) self.vx = 0; if (Math.abs(self.vy) < 0.03) self.vy = 0; // Wall collision if (self.x - self.radius < containerLeftX + wallThickness) { self.x = containerLeftX + wallThickness + self.radius; self.vx *= -0.5; } if (self.x + self.radius > containerRightX - wallThickness) { self.x = containerRightX - wallThickness - self.radius; self.vx *= -0.5; } // Floor collision if (self.y + self.radius > containerBottomY) { self.y = containerBottomY - self.radius; if (Math.abs(self.vy) > 2) { self.vy *= -0.45; } else { self.vy = 0; } // Friction self.vx *= 0.98; if (Math.abs(self.vx) < 0.1) self.vx = 0; } }; // Merge animation self.playMergeAnim = function () { tween(self, { scaleX: 1.25, scaleY: 1.25 }, { duration: 120, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 120, easing: tween.easeIn }); } }); }; // Destroy self.destroyFruit = function () { if (self.parent) self.parent.removeChild(self); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222233 }); /**** * Game Code ****/ /**** * Game Code // --- Shake Button --- var shakeBtn = new Text2("Shake!", { size: 110, fill: "#fff", font: "Impact, Arial Black, Tahoma" }); shakeBtn.anchor.set(0.5, 1); // Place at bottom center, above the very bottom (safe for mobile) shakeBtn.x = 2048 / 2; shakeBtn.y = 2732 - 80; shakeBtn.interactive = true; shakeBtn.buttonMode = true; LK.gui.bottom.addChild(shakeBtn); var isShaking = false; function shakeBottle() { if (isShaking) return; isShaking = true; // Animate containerBody, leftWall, rightWall, and all fruits var shakeAmount = 48; var shakeTime = 60; var shakeSeq = [ {x: -shakeAmount, duration: shakeTime}, {x: shakeAmount, duration: shakeTime * 2}, {x: -shakeAmount, duration: shakeTime * 2}, {x: 0, duration: shakeTime} ]; var targets = [containerBody, leftWall, rightWall]; // Save original X for all var origX = []; for (var i = 0; i < targets.length; i++) origX[i] = targets[i].x; // Fruits: only those inside the bottle var fruitOrigX = []; for (var i = 0; i < fruits.length; i++) fruitOrigX[i] = fruits[i].x; var step = 0; function doShakeStep() { if (step >= shakeSeq.length) { // Restore all X for (var i = 0; i < targets.length; i++) targets[i].x = origX[i]; for (var i = 0; i < fruits.length; i++) fruits[i].x = fruitOrigX[i]; isShaking = false; return; } var dx = shakeSeq[step].x; var dur = shakeSeq[step].duration; // Tween container and walls for (var i = 0; i < targets.length; i++) { tween(targets[i], {x: origX[i] + dx}, {duration: dur, easing: tween.cubicInOut}); } // Tween fruits for (var i = 0; i < fruits.length; i++) { tween(fruits[i], {x: fruitOrigX[i] + dx}, {duration: dur, easing: tween.cubicInOut}); // Add a little random nudge to vx for more realism fruits[i].vx += (Math.random() - 0.5) * 2.5; } LK.setTimeout(function () { step++; doShakeStep(); }, dur); } doShakeStep(); } // Button event: shake on down/tap shakeBtn.down = function(x, y, obj) { shakeBottle(); }; /**** * Fruit Progression Data ****/ // Container (the bin) // We'll use colored ellipses for each fruit, with increasing size and distinct colors. // Fruit progression: cherry โ strawberry โ grape โ dekopon โ orange โ apple โ pear โ peach โ pineapple โ melon โ watermelon // Container dimensions var FRUIT_LIST = [{ id: 'fruit_cherry', name: 'Cherry', score: 1 }, { id: 'fruit_strawberry', name: 'Strawberry', score: 2 }, { id: 'fruit_grape', name: 'Grape', score: 4 }, { id: 'fruit_dekopon', name: 'Dekopon', score: 8 }, { id: 'fruit_orange', name: 'Orange', score: 16 }, { id: 'fruit_apple', name: 'Apple', score: 32 }, { id: 'fruit_pear', name: 'Pear', score: 64 }, { id: 'fruit_peach', name: 'Peach', score: 128 }, { id: 'fruit_pineapple', name: 'Pineapple', score: 256 }, { id: 'fruit_melon', name: 'Melon', score: 512 }, { id: 'fruit_watermelon', name: 'Watermelon', score: 1024 }]; var containerWidth = 1200; var containerHeight = 1600; var wallThickness = 40; var containerLeftX = (2048 - containerWidth) / 2; var containerRightX = containerLeftX + containerWidth; var containerTopY = 400; var containerBottomY = containerTopY + containerHeight; // Draw container var containerBody = LK.getAsset('container_body', { anchorX: 0, anchorY: 0, x: containerLeftX, y: containerTopY }); game.addChild(containerBody); var leftWall = LK.getAsset('container_wall', { anchorX: 0, anchorY: 0, x: containerLeftX, y: containerTopY }); game.addChild(leftWall); var rightWall = LK.getAsset('container_wall', { anchorX: 0, anchorY: 0, x: containerRightX - wallThickness, y: containerTopY }); game.addChild(rightWall); // Fruits array var fruits = []; // Score var score = 0; var scoreTxt = new Text2('0', { size: 120, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Next fruit preview // The biggest dropable fruit is fruit_grape (index 2) var MAX_DROP_FRUIT_INDEX = 2; var nextFruitType = 0; var nextFruitAsset = LK.getAsset(FRUIT_LIST[nextFruitType].id, { anchorX: 0.5, anchorY: 0.5, // Place in right top corner, but not in the top left 100x100 area x: 2048 - 200, y: 180 }); game.addChild(nextFruitAsset); // Drop spot indicator line var dropLine = LK.getAsset('container_wall', { anchorX: 0.5, anchorY: 0, width: 8, height: containerHeight + 60, color: 0xffffff, x: 2048 / 2, y: containerTopY - 30 }); dropLine.alpha = 0.5; game.addChild(dropLine); // Current dropping fruit var currentFruit = null; // Helper: get random fruit type (up to fruit_grape) function getRandomFruitType() { // Only allow cherry, strawberry, grape (index 0,1,2) return Math.floor(Math.random() * (MAX_DROP_FRUIT_INDEX + 1)); } // Helper: spawn new fruit at top function spawnFruit() { var typeIdx = nextFruitType; currentFruit = new Fruit(); currentFruit.setType(typeIdx); currentFruit.setPosition(2048 / 2, containerTopY - 80); currentFruit.isDropping = false; currentFruit.vx = 0; currentFruit.vy = 0; currentFruit.scaleX = 1; currentFruit.scaleY = 1; game.addChild(currentFruit); // Move dropLine to center for new fruit if (dropLine) dropLine.x = 2048 / 2; // Next fruit nextFruitType = getRandomFruitType(); if (nextFruitAsset && nextFruitAsset.parent) nextFruitAsset.parent.removeChild(nextFruitAsset); nextFruitAsset = LK.getAsset(FRUIT_LIST[nextFruitType].id, { anchorX: 0.5, anchorY: 0.5, x: 2048 - 200, y: 180 }); game.addChild(nextFruitAsset); } // Helper: check collision between two fruits function fruitsCollide(f1, f2) { var dx = f1.x - f2.x; var dy = f1.y - f2.y; var dist = Math.sqrt(dx * dx + dy * dy); return dist < (f1.radius + f2.radius) * 0.98; } // Helper: resolve collision between two fruits (physics bounce) function resolveFruitCollision(f1, f2, pushStrength) { var dx = f1.x - f2.x; var dy = f1.y - f2.y; var dist = Math.sqrt(dx * dx + dy * dy); var minDist = f1.radius + f2.radius; if (dist < minDist && dist > 0.1) { var overlap = minDist - dist; var nx = dx / dist; var ny = dy / dist; // Push fruits apart (stronger separation for stability) var push = overlap / (pushStrength || 1.1); f1.x += nx * push / 2; f1.y += ny * push / 2; f2.x -= nx * push / 2; f2.y -= ny * push / 2; // Exchange velocity (dampen for stacking) var k = 0.45; var tx = nx; var ty = ny; var dp = (f1.vx - f2.vx) * tx + (f1.vy - f2.vy) * ty; if (dp < 0) { f1.vx -= dp * tx * k; f1.vy -= dp * ty * k; f2.vx += dp * tx * k; f2.vy += dp * ty * k; // Dampen vertical velocity for stacking f1.vy *= 0.82; f2.vy *= 0.82; } } } // Helper: merge two fruits (returns new fruit or null) function tryMergeFruits(f1, f2, tick) { if (f1.isMerging || f2.isMerging) return null; if (f1.fruitType !== f2.fruitType) return null; if (f1.fruitType >= FRUIT_LIST.length - 1) return null; // Prevent double merge in one tick if (f1.lastMergedTick === tick || f2.lastMergedTick === tick) return null; // Mark as merging f1.isMerging = true; f2.isMerging = true; f1.lastMergedTick = tick; f2.lastMergedTick = tick; // New fruit at average position var newFruit = new Fruit(); newFruit.setType(f1.fruitType + 1); newFruit.setPosition((f1.x + f2.x) / 2, (f1.y + f2.y) / 2); newFruit.vx = (f1.vx + f2.vx) / 2; newFruit.vy = (f1.vy + f2.vy) / 2 - 8; // bounce up a bit newFruit.isDropping = true; newFruit.scaleX = 0.7; newFruit.scaleY = 0.7; game.addChild(newFruit); // Merge animation newFruit.playMergeAnim(); // Particle effect for (var p = 0; p < 18; p++) { var particle = LK.getAsset(FRUIT_LIST[newFruit.fruitType].id, { anchorX: 0.5, anchorY: 0.5, x: newFruit.x, y: newFruit.y, scaleX: 0.18 + Math.random() * 0.12, scaleY: 0.18 + Math.random() * 0.12, alpha: 0.85 }); game.addChild(particle); var angle = Math.PI * 2 * p / 18 + Math.random() * 0.3; var dist = newFruit.radius * (1.1 + Math.random() * 0.7); var tx = newFruit.x + Math.cos(angle) * dist; var ty = newFruit.y + Math.sin(angle) * dist; tween(particle, { x: tx, y: ty, alpha: 0 }, { duration: 420 + Math.random() * 180, easing: tween.cubicOut, onFinish: function (pt) { return function () { if (pt.parent) pt.parent.removeChild(pt); }; }(particle) }); } // Explode nearby fruits outward for (var i = 0; i < fruits.length; i++) { var other = fruits[i]; if (other !== f1 && other !== f2 && other.isDropping) { var dx = other.x - newFruit.x; var dy = other.y - newFruit.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < newFruit.radius * 2.5 && dist > 0.1) { // Push away with random force var force = 18 + Math.random() * 8; var nx = dx / dist; var ny = dy / dist; other.vx += nx * force; other.vy += ny * force; } } } // Remove old fruits f1.destroyFruit(); f2.destroyFruit(); // Remove from fruits array for (var i = fruits.length - 1; i >= 0; i--) { if (fruits[i] === f1 || fruits[i] === f2) fruits.splice(i, 1); } fruits.push(newFruit); // Add score score += FRUIT_LIST[newFruit.fruitType].score; scoreTxt.setText(score); return newFruit; } // Helper: check for game over (if any fruit is above containerTopY) with 3-2-1 countdown (1 second per count) var gameOverCounter = 0; var gameOverCountdownActive = false; var countdownText = null; var gameOverInterval = null; function clearGameOverInterval() { if (gameOverInterval !== null) { LK.clearInterval(gameOverInterval); gameOverInterval = null; } } function checkGameOver() { var anyAbove = false; for (var i = 0; i < fruits.length; i++) { if (fruits[i].y - fruits[i].radius < containerTopY + 10) { anyAbove = true; break; } } if (anyAbove) { if (!gameOverCountdownActive) { gameOverCounter = 3; gameOverCountdownActive = true; // Show countdown text if (!countdownText) { countdownText = new Text2('3', { size: 220, fill: 0xFF3333 }); countdownText.anchor.set(0.5, 0.5); LK.gui.center.addChild(countdownText); } countdownText.setText("3"); countdownText.visible = true; // Start 1-second interval countdown clearGameOverInterval(); gameOverInterval = LK.setInterval(function () { // Check if still any fruit above var stillAbove = false; for (var i = 0; i < fruits.length; i++) { if (fruits[i].y - fruits[i].radius < containerTopY + 10) { stillAbove = true; break; } } if (!stillAbove) { // Reset if no fruit above clearGameOverInterval(); gameOverCounter = 0; gameOverCountdownActive = false; if (countdownText) countdownText.visible = false; return; } gameOverCounter--; if (countdownText) countdownText.setText(gameOverCounter > 0 ? String(gameOverCounter) : "0"); if (gameOverCounter <= 0) { clearGameOverInterval(); if (countdownText) countdownText.setText("0"); LK.effects.flashScreen(0xff0000, 1000); LK.setTimeout(function () { if (countdownText) countdownText.visible = false; LK.showGameOver(); }, 400); } }, 1000); } // else: already counting down, do nothing (interval handles decrement) } else { // No fruit above, reset everything clearGameOverInterval(); gameOverCounter = 0; gameOverCountdownActive = false; if (countdownText) countdownText.visible = false; } return false; } // Drag and drop for current fruit var isDragging = false; var dragOffsetX = 0; // Only allow drop inside container function clampFruitX(x, fruit) { var minX = containerLeftX + wallThickness + fruit.radius; var maxX = containerRightX - wallThickness - fruit.radius; if (x < minX) x = minX; if (x > maxX) x = maxX; return x; } // Touch/mouse down: start drag if on current fruit // Machine gun drop: hold to drop fruits rapidly var dropInterval = null; game.down = function (x, y, obj) { if (!currentFruit || currentFruit.isDropping) return; // Start interval for machine gun drop (hold to drop repeatedly) if (dropInterval) LK.clearInterval(dropInterval); dropInterval = LK.setInterval(function () { if (!currentFruit || currentFruit.isDropping) { LK.clearInterval(dropInterval); dropInterval = null; return; } // Drop and spawn on interval if holding currentFruit.isDropping = true; currentFruit.vx = 0; currentFruit.vy = 0; fruits.push(currentFruit); // Hide dropLine while fruit is falling if (dropLine) dropLine.visible = false; currentFruit = null; // Spawn next fruit after short delay LK.setTimeout(function () { if (dropLine) dropLine.visible = true; spawnFruit(); }, 120); }, 120); }; // Touch/mouse move: drag fruit horizontally game.move = function (x, y, obj) { if (!currentFruit || currentFruit.isDropping) return; // If dragging, keep old drag logic if (isDragging) { var nx = clampFruitX(x + dragOffsetX, currentFruit); currentFruit.x = nx; if (dropLine) dropLine.x = nx; } else { // Not dragging: always follow mouse/touch X axis var nx = clampFruitX(x, currentFruit); currentFruit.x = nx; if (dropLine) dropLine.x = nx; } }; // Touch/mouse up: drop fruit game.up = function (x, y, obj) { // Stop machine gun drop on release if (dropInterval) { LK.clearInterval(dropInterval); dropInterval = null; } if (!currentFruit || currentFruit.isDropping) return; // Always drop fruit on up, regardless of dragging isDragging = false; currentFruit.isDropping = true; currentFruit.vx = 0; currentFruit.vy = 0; fruits.push(currentFruit); // Hide dropLine while fruit is falling if (dropLine) dropLine.visible = false; currentFruit = null; // Spawn next fruit after short delay LK.setTimeout(function () { if (dropLine) dropLine.visible = true; spawnFruit(); }, 120); }; // Main update loop game.update = function () { // Update all fruits for (var i = 0; i < fruits.length; i++) { fruits[i].update(); } // Fruit-to-fruit collision and merging var tick = LK.ticks; // Run collision resolution more times per frame for much better separation for (var repeat = 0; repeat < 16; repeat++) { for (var i = 0; i < fruits.length; i++) { var f1 = fruits[i]; if (!f1.isDropping) continue; for (var j = i + 1; j < fruits.length; j++) { var f2 = fruits[j]; if (!f2.isDropping) continue; if (fruitsCollide(f1, f2)) { // Try merge only on first pass if (repeat === 0) { var merged = tryMergeFruits(f1, f2, tick); if (merged) break; // f1 is gone, break inner loop } // Always resolve collision // Use a much stronger push for the first several passes resolveFruitCollision(f1, f2, repeat < 6 ? 2.8 : 1.2); } } } } // Check for game over // Only check for game over if there is no currentFruit (i.e., all fruits are dropped) // And only after a 3 second grace period after a fruit is dropped if (!currentFruit) { if (typeof lastFruitDropTime === "undefined") lastFruitDropTime = 0; if (typeof gameOverGraceActive === "undefined") gameOverGraceActive = false; if (!gameOverGraceActive) { // Start grace period after fruit is dropped lastFruitDropTime = Date.now(); gameOverGraceActive = true; } // Wait 3 seconds after drop before checking game over if (Date.now() - lastFruitDropTime > 3000) { checkGameOver(); } } else { // Reset grace period if a new fruit is being held gameOverGraceActive = false; } }; // Start game score = 0; scoreTxt.setText(score); fruits = []; gameOverCounter = 0; gameOverCountdownActive = false; if (typeof countdownText !== "undefined" && countdownText && countdownText.parent) { countdownText.visible = false; } if (nextFruitAsset && nextFruitAsset.parent) nextFruitAsset.parent.removeChild(nextFruitAsset); nextFruitType = getRandomFruitType(); nextFruitAsset = LK.getAsset(FRUIT_LIST[nextFruitType].id, { anchorX: 0.5, anchorY: 0.5, x: 2048 - 200, y: 180 }); game.addChild(nextFruitAsset); spawnFruit();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Fruit class
var Fruit = Container.expand(function () {
var self = Container.call(this);
// Properties
self.fruitType = 0; // index in FRUIT_LIST
self.radius = 60; // default, will be set below
self.vx = 0;
self.vy = 0;
self.isDropping = false; // true if falling, false if held at top
self.isMerging = false; // true if currently merging (to avoid double merges)
self.lastMergedTick = -1; // to prevent double merges in one tick
// Attach fruit asset
self.setType = function (typeIdx) {
self.fruitType = typeIdx;
if (self.fruitAsset) {
self.removeChild(self.fruitAsset);
}
var fruitId = FRUIT_LIST[typeIdx].id;
self.fruitAsset = self.attachAsset(fruitId, {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = self.fruitAsset.width / 2;
};
// Set initial type
self.setType(0);
// Set position
self.setPosition = function (x, y) {
self.x = x;
self.y = y;
};
// Physics update
self.update = function () {
if (!self.isDropping) return;
// Gravity
self.vy += 0.62; // reduced gravity for more stable stacking and less shake
// Clamp vy
if (self.vy > 16) self.vy = 16;
// Move
self.x += self.vx;
self.y += self.vy;
// Dampen tiny velocities to avoid jitter
if (Math.abs(self.vx) < 0.03) self.vx = 0;
if (Math.abs(self.vy) < 0.03) self.vy = 0;
// Wall collision
if (self.x - self.radius < containerLeftX + wallThickness) {
self.x = containerLeftX + wallThickness + self.radius;
self.vx *= -0.5;
}
if (self.x + self.radius > containerRightX - wallThickness) {
self.x = containerRightX - wallThickness - self.radius;
self.vx *= -0.5;
}
// Floor collision
if (self.y + self.radius > containerBottomY) {
self.y = containerBottomY - self.radius;
if (Math.abs(self.vy) > 2) {
self.vy *= -0.45;
} else {
self.vy = 0;
}
// Friction
self.vx *= 0.98;
if (Math.abs(self.vx) < 0.1) self.vx = 0;
}
};
// Merge animation
self.playMergeAnim = function () {
tween(self, {
scaleX: 1.25,
scaleY: 1.25
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 120,
easing: tween.easeIn
});
}
});
};
// Destroy
self.destroyFruit = function () {
if (self.parent) self.parent.removeChild(self);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222233
});
/****
* Game Code
****/
/****
* Game Code
// --- Shake Button ---
var shakeBtn = new Text2("Shake!", {
size: 110,
fill: "#fff",
font: "Impact, Arial Black, Tahoma"
});
shakeBtn.anchor.set(0.5, 1);
// Place at bottom center, above the very bottom (safe for mobile)
shakeBtn.x = 2048 / 2;
shakeBtn.y = 2732 - 80;
shakeBtn.interactive = true;
shakeBtn.buttonMode = true;
LK.gui.bottom.addChild(shakeBtn);
var isShaking = false;
function shakeBottle() {
if (isShaking) return;
isShaking = true;
// Animate containerBody, leftWall, rightWall, and all fruits
var shakeAmount = 48;
var shakeTime = 60;
var shakeSeq = [
{x: -shakeAmount, duration: shakeTime},
{x: shakeAmount, duration: shakeTime * 2},
{x: -shakeAmount, duration: shakeTime * 2},
{x: 0, duration: shakeTime}
];
var targets = [containerBody, leftWall, rightWall];
// Save original X for all
var origX = [];
for (var i = 0; i < targets.length; i++) origX[i] = targets[i].x;
// Fruits: only those inside the bottle
var fruitOrigX = [];
for (var i = 0; i < fruits.length; i++) fruitOrigX[i] = fruits[i].x;
var step = 0;
function doShakeStep() {
if (step >= shakeSeq.length) {
// Restore all X
for (var i = 0; i < targets.length; i++) targets[i].x = origX[i];
for (var i = 0; i < fruits.length; i++) fruits[i].x = fruitOrigX[i];
isShaking = false;
return;
}
var dx = shakeSeq[step].x;
var dur = shakeSeq[step].duration;
// Tween container and walls
for (var i = 0; i < targets.length; i++) {
tween(targets[i], {x: origX[i] + dx}, {duration: dur, easing: tween.cubicInOut});
}
// Tween fruits
for (var i = 0; i < fruits.length; i++) {
tween(fruits[i], {x: fruitOrigX[i] + dx}, {duration: dur, easing: tween.cubicInOut});
// Add a little random nudge to vx for more realism
fruits[i].vx += (Math.random() - 0.5) * 2.5;
}
LK.setTimeout(function () {
step++;
doShakeStep();
}, dur);
}
doShakeStep();
}
// Button event: shake on down/tap
shakeBtn.down = function(x, y, obj) {
shakeBottle();
};
/****
* Fruit Progression Data
****/
// Container (the bin)
// We'll use colored ellipses for each fruit, with increasing size and distinct colors.
// Fruit progression: cherry โ strawberry โ grape โ dekopon โ orange โ apple โ pear โ peach โ pineapple โ melon โ watermelon
// Container dimensions
var FRUIT_LIST = [{
id: 'fruit_cherry',
name: 'Cherry',
score: 1
}, {
id: 'fruit_strawberry',
name: 'Strawberry',
score: 2
}, {
id: 'fruit_grape',
name: 'Grape',
score: 4
}, {
id: 'fruit_dekopon',
name: 'Dekopon',
score: 8
}, {
id: 'fruit_orange',
name: 'Orange',
score: 16
}, {
id: 'fruit_apple',
name: 'Apple',
score: 32
}, {
id: 'fruit_pear',
name: 'Pear',
score: 64
}, {
id: 'fruit_peach',
name: 'Peach',
score: 128
}, {
id: 'fruit_pineapple',
name: 'Pineapple',
score: 256
}, {
id: 'fruit_melon',
name: 'Melon',
score: 512
}, {
id: 'fruit_watermelon',
name: 'Watermelon',
score: 1024
}];
var containerWidth = 1200;
var containerHeight = 1600;
var wallThickness = 40;
var containerLeftX = (2048 - containerWidth) / 2;
var containerRightX = containerLeftX + containerWidth;
var containerTopY = 400;
var containerBottomY = containerTopY + containerHeight;
// Draw container
var containerBody = LK.getAsset('container_body', {
anchorX: 0,
anchorY: 0,
x: containerLeftX,
y: containerTopY
});
game.addChild(containerBody);
var leftWall = LK.getAsset('container_wall', {
anchorX: 0,
anchorY: 0,
x: containerLeftX,
y: containerTopY
});
game.addChild(leftWall);
var rightWall = LK.getAsset('container_wall', {
anchorX: 0,
anchorY: 0,
x: containerRightX - wallThickness,
y: containerTopY
});
game.addChild(rightWall);
// Fruits array
var fruits = [];
// Score
var score = 0;
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Next fruit preview
// The biggest dropable fruit is fruit_grape (index 2)
var MAX_DROP_FRUIT_INDEX = 2;
var nextFruitType = 0;
var nextFruitAsset = LK.getAsset(FRUIT_LIST[nextFruitType].id, {
anchorX: 0.5,
anchorY: 0.5,
// Place in right top corner, but not in the top left 100x100 area
x: 2048 - 200,
y: 180
});
game.addChild(nextFruitAsset);
// Drop spot indicator line
var dropLine = LK.getAsset('container_wall', {
anchorX: 0.5,
anchorY: 0,
width: 8,
height: containerHeight + 60,
color: 0xffffff,
x: 2048 / 2,
y: containerTopY - 30
});
dropLine.alpha = 0.5;
game.addChild(dropLine);
// Current dropping fruit
var currentFruit = null;
// Helper: get random fruit type (up to fruit_grape)
function getRandomFruitType() {
// Only allow cherry, strawberry, grape (index 0,1,2)
return Math.floor(Math.random() * (MAX_DROP_FRUIT_INDEX + 1));
}
// Helper: spawn new fruit at top
function spawnFruit() {
var typeIdx = nextFruitType;
currentFruit = new Fruit();
currentFruit.setType(typeIdx);
currentFruit.setPosition(2048 / 2, containerTopY - 80);
currentFruit.isDropping = false;
currentFruit.vx = 0;
currentFruit.vy = 0;
currentFruit.scaleX = 1;
currentFruit.scaleY = 1;
game.addChild(currentFruit);
// Move dropLine to center for new fruit
if (dropLine) dropLine.x = 2048 / 2;
// Next fruit
nextFruitType = getRandomFruitType();
if (nextFruitAsset && nextFruitAsset.parent) nextFruitAsset.parent.removeChild(nextFruitAsset);
nextFruitAsset = LK.getAsset(FRUIT_LIST[nextFruitType].id, {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 180
});
game.addChild(nextFruitAsset);
}
// Helper: check collision between two fruits
function fruitsCollide(f1, f2) {
var dx = f1.x - f2.x;
var dy = f1.y - f2.y;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < (f1.radius + f2.radius) * 0.98;
}
// Helper: resolve collision between two fruits (physics bounce)
function resolveFruitCollision(f1, f2, pushStrength) {
var dx = f1.x - f2.x;
var dy = f1.y - f2.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var minDist = f1.radius + f2.radius;
if (dist < minDist && dist > 0.1) {
var overlap = minDist - dist;
var nx = dx / dist;
var ny = dy / dist;
// Push fruits apart (stronger separation for stability)
var push = overlap / (pushStrength || 1.1);
f1.x += nx * push / 2;
f1.y += ny * push / 2;
f2.x -= nx * push / 2;
f2.y -= ny * push / 2;
// Exchange velocity (dampen for stacking)
var k = 0.45;
var tx = nx;
var ty = ny;
var dp = (f1.vx - f2.vx) * tx + (f1.vy - f2.vy) * ty;
if (dp < 0) {
f1.vx -= dp * tx * k;
f1.vy -= dp * ty * k;
f2.vx += dp * tx * k;
f2.vy += dp * ty * k;
// Dampen vertical velocity for stacking
f1.vy *= 0.82;
f2.vy *= 0.82;
}
}
}
// Helper: merge two fruits (returns new fruit or null)
function tryMergeFruits(f1, f2, tick) {
if (f1.isMerging || f2.isMerging) return null;
if (f1.fruitType !== f2.fruitType) return null;
if (f1.fruitType >= FRUIT_LIST.length - 1) return null;
// Prevent double merge in one tick
if (f1.lastMergedTick === tick || f2.lastMergedTick === tick) return null;
// Mark as merging
f1.isMerging = true;
f2.isMerging = true;
f1.lastMergedTick = tick;
f2.lastMergedTick = tick;
// New fruit at average position
var newFruit = new Fruit();
newFruit.setType(f1.fruitType + 1);
newFruit.setPosition((f1.x + f2.x) / 2, (f1.y + f2.y) / 2);
newFruit.vx = (f1.vx + f2.vx) / 2;
newFruit.vy = (f1.vy + f2.vy) / 2 - 8; // bounce up a bit
newFruit.isDropping = true;
newFruit.scaleX = 0.7;
newFruit.scaleY = 0.7;
game.addChild(newFruit);
// Merge animation
newFruit.playMergeAnim();
// Particle effect
for (var p = 0; p < 18; p++) {
var particle = LK.getAsset(FRUIT_LIST[newFruit.fruitType].id, {
anchorX: 0.5,
anchorY: 0.5,
x: newFruit.x,
y: newFruit.y,
scaleX: 0.18 + Math.random() * 0.12,
scaleY: 0.18 + Math.random() * 0.12,
alpha: 0.85
});
game.addChild(particle);
var angle = Math.PI * 2 * p / 18 + Math.random() * 0.3;
var dist = newFruit.radius * (1.1 + Math.random() * 0.7);
var tx = newFruit.x + Math.cos(angle) * dist;
var ty = newFruit.y + Math.sin(angle) * dist;
tween(particle, {
x: tx,
y: ty,
alpha: 0
}, {
duration: 420 + Math.random() * 180,
easing: tween.cubicOut,
onFinish: function (pt) {
return function () {
if (pt.parent) pt.parent.removeChild(pt);
};
}(particle)
});
}
// Explode nearby fruits outward
for (var i = 0; i < fruits.length; i++) {
var other = fruits[i];
if (other !== f1 && other !== f2 && other.isDropping) {
var dx = other.x - newFruit.x;
var dy = other.y - newFruit.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < newFruit.radius * 2.5 && dist > 0.1) {
// Push away with random force
var force = 18 + Math.random() * 8;
var nx = dx / dist;
var ny = dy / dist;
other.vx += nx * force;
other.vy += ny * force;
}
}
}
// Remove old fruits
f1.destroyFruit();
f2.destroyFruit();
// Remove from fruits array
for (var i = fruits.length - 1; i >= 0; i--) {
if (fruits[i] === f1 || fruits[i] === f2) fruits.splice(i, 1);
}
fruits.push(newFruit);
// Add score
score += FRUIT_LIST[newFruit.fruitType].score;
scoreTxt.setText(score);
return newFruit;
}
// Helper: check for game over (if any fruit is above containerTopY) with 3-2-1 countdown (1 second per count)
var gameOverCounter = 0;
var gameOverCountdownActive = false;
var countdownText = null;
var gameOverInterval = null;
function clearGameOverInterval() {
if (gameOverInterval !== null) {
LK.clearInterval(gameOverInterval);
gameOverInterval = null;
}
}
function checkGameOver() {
var anyAbove = false;
for (var i = 0; i < fruits.length; i++) {
if (fruits[i].y - fruits[i].radius < containerTopY + 10) {
anyAbove = true;
break;
}
}
if (anyAbove) {
if (!gameOverCountdownActive) {
gameOverCounter = 3;
gameOverCountdownActive = true;
// Show countdown text
if (!countdownText) {
countdownText = new Text2('3', {
size: 220,
fill: 0xFF3333
});
countdownText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(countdownText);
}
countdownText.setText("3");
countdownText.visible = true;
// Start 1-second interval countdown
clearGameOverInterval();
gameOverInterval = LK.setInterval(function () {
// Check if still any fruit above
var stillAbove = false;
for (var i = 0; i < fruits.length; i++) {
if (fruits[i].y - fruits[i].radius < containerTopY + 10) {
stillAbove = true;
break;
}
}
if (!stillAbove) {
// Reset if no fruit above
clearGameOverInterval();
gameOverCounter = 0;
gameOverCountdownActive = false;
if (countdownText) countdownText.visible = false;
return;
}
gameOverCounter--;
if (countdownText) countdownText.setText(gameOverCounter > 0 ? String(gameOverCounter) : "0");
if (gameOverCounter <= 0) {
clearGameOverInterval();
if (countdownText) countdownText.setText("0");
LK.effects.flashScreen(0xff0000, 1000);
LK.setTimeout(function () {
if (countdownText) countdownText.visible = false;
LK.showGameOver();
}, 400);
}
}, 1000);
}
// else: already counting down, do nothing (interval handles decrement)
} else {
// No fruit above, reset everything
clearGameOverInterval();
gameOverCounter = 0;
gameOverCountdownActive = false;
if (countdownText) countdownText.visible = false;
}
return false;
}
// Drag and drop for current fruit
var isDragging = false;
var dragOffsetX = 0;
// Only allow drop inside container
function clampFruitX(x, fruit) {
var minX = containerLeftX + wallThickness + fruit.radius;
var maxX = containerRightX - wallThickness - fruit.radius;
if (x < minX) x = minX;
if (x > maxX) x = maxX;
return x;
}
// Touch/mouse down: start drag if on current fruit
// Machine gun drop: hold to drop fruits rapidly
var dropInterval = null;
game.down = function (x, y, obj) {
if (!currentFruit || currentFruit.isDropping) return;
// Start interval for machine gun drop (hold to drop repeatedly)
if (dropInterval) LK.clearInterval(dropInterval);
dropInterval = LK.setInterval(function () {
if (!currentFruit || currentFruit.isDropping) {
LK.clearInterval(dropInterval);
dropInterval = null;
return;
}
// Drop and spawn on interval if holding
currentFruit.isDropping = true;
currentFruit.vx = 0;
currentFruit.vy = 0;
fruits.push(currentFruit);
// Hide dropLine while fruit is falling
if (dropLine) dropLine.visible = false;
currentFruit = null;
// Spawn next fruit after short delay
LK.setTimeout(function () {
if (dropLine) dropLine.visible = true;
spawnFruit();
}, 120);
}, 120);
};
// Touch/mouse move: drag fruit horizontally
game.move = function (x, y, obj) {
if (!currentFruit || currentFruit.isDropping) return;
// If dragging, keep old drag logic
if (isDragging) {
var nx = clampFruitX(x + dragOffsetX, currentFruit);
currentFruit.x = nx;
if (dropLine) dropLine.x = nx;
} else {
// Not dragging: always follow mouse/touch X axis
var nx = clampFruitX(x, currentFruit);
currentFruit.x = nx;
if (dropLine) dropLine.x = nx;
}
};
// Touch/mouse up: drop fruit
game.up = function (x, y, obj) {
// Stop machine gun drop on release
if (dropInterval) {
LK.clearInterval(dropInterval);
dropInterval = null;
}
if (!currentFruit || currentFruit.isDropping) return;
// Always drop fruit on up, regardless of dragging
isDragging = false;
currentFruit.isDropping = true;
currentFruit.vx = 0;
currentFruit.vy = 0;
fruits.push(currentFruit);
// Hide dropLine while fruit is falling
if (dropLine) dropLine.visible = false;
currentFruit = null;
// Spawn next fruit after short delay
LK.setTimeout(function () {
if (dropLine) dropLine.visible = true;
spawnFruit();
}, 120);
};
// Main update loop
game.update = function () {
// Update all fruits
for (var i = 0; i < fruits.length; i++) {
fruits[i].update();
}
// Fruit-to-fruit collision and merging
var tick = LK.ticks;
// Run collision resolution more times per frame for much better separation
for (var repeat = 0; repeat < 16; repeat++) {
for (var i = 0; i < fruits.length; i++) {
var f1 = fruits[i];
if (!f1.isDropping) continue;
for (var j = i + 1; j < fruits.length; j++) {
var f2 = fruits[j];
if (!f2.isDropping) continue;
if (fruitsCollide(f1, f2)) {
// Try merge only on first pass
if (repeat === 0) {
var merged = tryMergeFruits(f1, f2, tick);
if (merged) break; // f1 is gone, break inner loop
}
// Always resolve collision
// Use a much stronger push for the first several passes
resolveFruitCollision(f1, f2, repeat < 6 ? 2.8 : 1.2);
}
}
}
}
// Check for game over
// Only check for game over if there is no currentFruit (i.e., all fruits are dropped)
// And only after a 3 second grace period after a fruit is dropped
if (!currentFruit) {
if (typeof lastFruitDropTime === "undefined") lastFruitDropTime = 0;
if (typeof gameOverGraceActive === "undefined") gameOverGraceActive = false;
if (!gameOverGraceActive) {
// Start grace period after fruit is dropped
lastFruitDropTime = Date.now();
gameOverGraceActive = true;
}
// Wait 3 seconds after drop before checking game over
if (Date.now() - lastFruitDropTime > 3000) {
checkGameOver();
}
} else {
// Reset grace period if a new fruit is being held
gameOverGraceActive = false;
}
};
// Start game
score = 0;
scoreTxt.setText(score);
fruits = [];
gameOverCounter = 0;
gameOverCountdownActive = false;
if (typeof countdownText !== "undefined" && countdownText && countdownText.parent) {
countdownText.visible = false;
}
if (nextFruitAsset && nextFruitAsset.parent) nextFruitAsset.parent.removeChild(nextFruitAsset);
nextFruitType = getRandomFruitType();
nextFruitAsset = LK.getAsset(FRUIT_LIST[nextFruitType].id, {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 180
});
game.addChild(nextFruitAsset);
spawnFruit();