/**** * 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();