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