/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var FallenBottle = Container.expand(function () {
var self = Container.call(this);
var bottleGraphics = self.attachAsset('perfumeBottleUpright', {
anchorX: 0.5,
anchorY: 1.0
});
self.placed = false;
self.fallSpeed = 8;
self.update = function () {
if (!self.placed) {
self.y += self.fallSpeed;
}
};
return self;
});
var SwingingBottle = Container.expand(function () {
var self = Container.call(this);
// Create rope
var rope = self.attachAsset('rope', {
anchorX: 0.5,
anchorY: 0
});
rope.y = -150;
// Create bottle
var bottleGraphics = self.attachAsset('perfumeBottleUpright', {
anchorX: 0.5,
anchorY: 0
});
bottleGraphics.y = 150;
// Rope physics properties
self.angle = 0;
self.angularVelocity = 0;
self.ropeLength = 300;
self.swingSpeed = 0.02;
self.gravity = 0.0005;
self.damping = 0.998;
self.maxSwingAngle = Math.PI / 4; // 45 degrees max swing
// State
self.isSwinging = true;
self.fallSpeed = 5;
self.placed = false;
self.update = function () {
if (self.isSwinging) {
// Enhanced pendulum physics with faster swing
self.angularVelocity += -self.gravity * Math.sin(self.angle) * 2.5;
self.angularVelocity *= self.damping;
self.angle += self.angularVelocity;
// Limit swing angle
if (Math.abs(self.angle) > self.maxSwingAngle) {
self.angle = self.maxSwingAngle * Math.sign(self.angle);
self.angularVelocity *= -0.8;
}
// Apply swing position to both rope and bottle in same direction with equal movement
var swingOffset = Math.sin(self.angle) * self.ropeLength * 0.8;
bottleGraphics.x = swingOffset;
rope.x = swingOffset;
rope.rotation = self.angle;
} else if (!self.placed) {
// Falling
self.y += self.fallSpeed;
}
};
self.releaseFromRope = function () {
self.isSwinging = false;
// Remove rope visual
if (rope.parent) {
rope.parent.removeChild(rope);
}
};
return self;
});
var Tower = Container.expand(function () {
var self = Container.call(this);
self.bottles = [];
self.baseHeight = 0;
self.swayAngle = 0;
self.swayVelocity = 0;
self.isSwaying = false;
self.swayIntensity = 0;
self.lastAlignment = 0;
// Add base platform
self.addBase = function () {
var base = self.attachAsset('bottleBase', {
anchorX: 0.5,
anchorY: 1.0
});
base.y = gameHeight - 50; // Position closer to bottom
base.x = 0; // Center the base
self.baseHeight = base.y;
};
self.addBottle = function (bottle) {
// Position bottle on top of stack
if (self.bottles.length === 0) {
// First bottle lands directly on the base
bottle.y = self.baseHeight;
} else {
// Stack subsequent bottles
bottle.y = self.baseHeight - self.bottles.length * 200;
}
// Keep bottle at its current x position (where it landed)
bottle.placed = true;
self.bottles.push(bottle);
};
self.getStackHeight = function () {
// First bottle lands directly on the base
if (self.bottles.length === 0) {
return self.baseHeight;
}
// Subsequent bottles stack on top
return self.baseHeight - self.bottles.length * 200;
};
self.checkValidPlacement = function (bottle) {
// Check if bottle lands close enough to tower position
// Tolerance decreases with difficulty
var baseTolerance = 200;
var tolerance = Math.max(80, baseTolerance - (currentDifficulty - 1) * 30);
return Math.abs(bottle.x - self.x) <= tolerance;
};
self.calculateAlignment = function () {
if (self.bottles.length < 2) return 1; // Perfect alignment for less than 2 bottles
var totalDeviation = 0;
var centerX = self.x;
for (var i = 0; i < self.bottles.length; i++) {
totalDeviation += Math.abs(self.bottles[i].x - centerX);
}
var averageDeviation = totalDeviation / self.bottles.length;
var maxAllowedDeviation = 100; // Perfect alignment threshold
var alignment = Math.max(0, 1 - averageDeviation / maxAllowedDeviation);
return alignment;
};
self.updateSway = function () {
var alignment = self.calculateAlignment();
self.lastAlignment = alignment;
// Check for tower collapse if very unstable
if (self.bottles.length >= 3 && alignment < 0.3) {
self.collapseTower();
return;
}
// Start swaying if we have 5+ bottles and alignment is poor
if (self.bottles.length >= 5) {
var swayThreshold = 0.7; // Start swaying when alignment drops below 70%
if (alignment < swayThreshold) {
self.isSwaying = true;
self.swayIntensity = Math.max(0.5, 1 - alignment); // More misalignment = more sway
} else {
// Good alignment reduces sway
self.swayIntensity *= 0.95;
if (self.swayIntensity < 0.1) {
self.isSwaying = false;
self.swayIntensity = 0;
}
}
}
if (self.isSwaying) {
// Apply sway physics with increased speed
self.swayVelocity += Math.sin(LK.ticks * 0.15) * 0.0008 * self.swayIntensity;
self.swayVelocity *= 0.98; // Damping
self.swayAngle += self.swayVelocity;
self.swayAngle *= 0.99; // Angle damping
// Apply sway to tower position
var maxSway = 30 * self.swayIntensity;
self.x = gameWidth / 2 + Math.sin(self.swayAngle) * maxSway;
// Apply sway rotation to all bottles
for (var i = 0; i < self.bottles.length; i++) {
var bottle = self.bottles[i];
var swayRotation = Math.sin(self.swayAngle) * 0.05 * self.swayIntensity;
bottle.rotation = swayRotation;
}
}
};
self.collapseTower = function () {
// Animate bottles falling
for (var i = 0; i < self.bottles.length; i++) {
var bottle = self.bottles[i];
var randomX = bottle.x + (Math.random() - 0.5) * 300;
var randomRotation = (Math.random() - 0.5) * Math.PI;
tween(bottle, {
x: randomX,
y: gameHeight + 200,
rotation: randomRotation,
alpha: 0
}, {
duration: 1000 + Math.random() * 500,
easing: tween.easeIn
});
}
// Trigger game over after animation
LK.setTimeout(function () {
gameActive = false;
LK.effects.flashScreen(0xFF0000, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}, 500);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xF5F0E8
});
/****
* Game Code
****/
var gameWidth = 2048;
var gameHeight = 2732;
// Game state variables
var score = 0;
var gameActive = true;
var swingingBottle = null;
var fallingBottle = null;
var towersBuilt = 0;
var currentDifficulty = 1;
// Create tower
var tower = game.addChild(new Tower());
tower.x = gameWidth / 2;
tower.addBase();
// Create score display
var scoreTxt = new Text2('Score: 0', {
size: 80,
fill: 0x8B4513
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Create instruction text
var instructionTxt = new Text2('Touch to release bottle', {
size: 60,
fill: 0xA0522D
});
instructionTxt.anchor.set(0.5, 0);
instructionTxt.y = 120;
LK.gui.top.addChild(instructionTxt);
// Spawn new swinging bottle
function spawnSwingingBottle() {
if (!gameActive || swingingBottle) {
return;
}
swingingBottle = new SwingingBottle();
swingingBottle.x = gameWidth / 2; // Center horizontally
swingingBottle.y = 150; // Position rope anchor point higher
// Apply difficulty scaling
var difficultyMultiplier = 1 + (currentDifficulty - 1) * 0.3;
swingingBottle.swingSpeed *= difficultyMultiplier;
swingingBottle.gravity *= difficultyMultiplier;
swingingBottle.maxSwingAngle = Math.min(Math.PI / 3, Math.PI / 4 * difficultyMultiplier);
// Add random initial swing with increased randomness based on difficulty
var randomness = 0.5 + (currentDifficulty - 1) * 0.2;
swingingBottle.angle = (Math.random() - 0.5) * randomness;
swingingBottle.angularVelocity = (Math.random() - 0.5) * 0.01 * difficultyMultiplier;
game.addChild(swingingBottle);
}
// Release bottle from rope
function releaseBottle() {
if (!swingingBottle || !gameActive) {
return;
}
// Create falling bottle at current swing position
fallingBottle = new FallenBottle();
// Get the actual world position of the swinging bottle
var swingOffset = Math.sin(swingingBottle.angle) * swingingBottle.ropeLength;
fallingBottle.x = swingingBottle.x + swingOffset;
fallingBottle.y = swingingBottle.y + 150; // Account for rope length
game.addChild(fallingBottle);
// Remove swinging bottle
swingingBottle.destroy();
swingingBottle = null;
LK.getSound('placeBottle').play();
}
// Check bottle landing
function checkBottleLanding() {
if (!fallingBottle || fallingBottle.placed) {
return;
}
var expectedLandingHeight = tower.getStackHeight();
// Check if bottle reached landing position
if (fallingBottle.y >= expectedLandingHeight) {
// Check if placement is valid
if (tower.checkValidPlacement(fallingBottle)) {
// Successful placement
tower.addBottle(fallingBottle);
// Create spray effect
var sprayEffect = game.addChild(LK.getAsset('sprayEffect', {
anchorX: 0.5,
anchorY: 0.5
}));
sprayEffect.x = fallingBottle.x;
sprayEffect.y = fallingBottle.y - 100;
sprayEffect.alpha = 0.8;
sprayEffect.scaleX = 0.5;
sprayEffect.scaleY = 0.5;
// Animate spray effect
tween(sprayEffect, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
sprayEffect.destroy();
}
});
fallingBottle = null;
// Calculate score based on alignment
var alignment = tower.calculateAlignment();
var bottleScore = 10; // Base score
if (alignment >= 0.9 && tower.bottles.length === 1) {
bottleScore = 50; // Perfect alignment bonus only for first bottle
}
score += bottleScore;
LK.setScore(score);
scoreTxt.setText('Score: ' + score);
// Check if tower is complete (every 5 bottles = 1 tower)
if (tower.bottles.length % 5 === 0) {
towersBuilt++;
currentDifficulty = Math.min(5, Math.floor(towersBuilt / 2) + 1); // Increase difficulty every 2 towers, max level 5
// Show tower completion effect with different colors based on alignment
var flashColor = alignment >= 0.9 ? 0xFFD700 : 0x90EE90; // Gold for perfect, green for good
LK.effects.flashScreen(flashColor, 500);
}
// Spawn next bottle after delay (shorter delay at higher difficulties)
var spawnDelay = Math.max(500, 1000 - (currentDifficulty - 1) * 100);
LK.setTimeout(function () {
spawnSwingingBottle();
}, spawnDelay);
} else {
// Missed placement - game over
gameActive = false;
LK.effects.flashScreen(0xFF6B6B, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}
}
}
// Game input handling
game.down = function (x, y, obj) {
releaseBottle();
};
// Main game update loop
game.update = function () {
if (!gameActive) {
return;
}
// Update tower sway
tower.updateSway();
// Check bottle landing
checkBottleLanding();
// Check if falling bottle has gone off screen
if (fallingBottle && fallingBottle.y > gameHeight + 100) {
// Missed bottle - game over
gameActive = false;
LK.effects.flashScreen(0xFF6B6B, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}
};
// Start the game by spawning first swinging bottle
LK.setTimeout(function () {
spawnSwingingBottle();
}, 1000); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var FallenBottle = Container.expand(function () {
var self = Container.call(this);
var bottleGraphics = self.attachAsset('perfumeBottleUpright', {
anchorX: 0.5,
anchorY: 1.0
});
self.placed = false;
self.fallSpeed = 8;
self.update = function () {
if (!self.placed) {
self.y += self.fallSpeed;
}
};
return self;
});
var SwingingBottle = Container.expand(function () {
var self = Container.call(this);
// Create rope
var rope = self.attachAsset('rope', {
anchorX: 0.5,
anchorY: 0
});
rope.y = -150;
// Create bottle
var bottleGraphics = self.attachAsset('perfumeBottleUpright', {
anchorX: 0.5,
anchorY: 0
});
bottleGraphics.y = 150;
// Rope physics properties
self.angle = 0;
self.angularVelocity = 0;
self.ropeLength = 300;
self.swingSpeed = 0.02;
self.gravity = 0.0005;
self.damping = 0.998;
self.maxSwingAngle = Math.PI / 4; // 45 degrees max swing
// State
self.isSwinging = true;
self.fallSpeed = 5;
self.placed = false;
self.update = function () {
if (self.isSwinging) {
// Enhanced pendulum physics with faster swing
self.angularVelocity += -self.gravity * Math.sin(self.angle) * 2.5;
self.angularVelocity *= self.damping;
self.angle += self.angularVelocity;
// Limit swing angle
if (Math.abs(self.angle) > self.maxSwingAngle) {
self.angle = self.maxSwingAngle * Math.sign(self.angle);
self.angularVelocity *= -0.8;
}
// Apply swing position to both rope and bottle in same direction with equal movement
var swingOffset = Math.sin(self.angle) * self.ropeLength * 0.8;
bottleGraphics.x = swingOffset;
rope.x = swingOffset;
rope.rotation = self.angle;
} else if (!self.placed) {
// Falling
self.y += self.fallSpeed;
}
};
self.releaseFromRope = function () {
self.isSwinging = false;
// Remove rope visual
if (rope.parent) {
rope.parent.removeChild(rope);
}
};
return self;
});
var Tower = Container.expand(function () {
var self = Container.call(this);
self.bottles = [];
self.baseHeight = 0;
self.swayAngle = 0;
self.swayVelocity = 0;
self.isSwaying = false;
self.swayIntensity = 0;
self.lastAlignment = 0;
// Add base platform
self.addBase = function () {
var base = self.attachAsset('bottleBase', {
anchorX: 0.5,
anchorY: 1.0
});
base.y = gameHeight - 50; // Position closer to bottom
base.x = 0; // Center the base
self.baseHeight = base.y;
};
self.addBottle = function (bottle) {
// Position bottle on top of stack
if (self.bottles.length === 0) {
// First bottle lands directly on the base
bottle.y = self.baseHeight;
} else {
// Stack subsequent bottles
bottle.y = self.baseHeight - self.bottles.length * 200;
}
// Keep bottle at its current x position (where it landed)
bottle.placed = true;
self.bottles.push(bottle);
};
self.getStackHeight = function () {
// First bottle lands directly on the base
if (self.bottles.length === 0) {
return self.baseHeight;
}
// Subsequent bottles stack on top
return self.baseHeight - self.bottles.length * 200;
};
self.checkValidPlacement = function (bottle) {
// Check if bottle lands close enough to tower position
// Tolerance decreases with difficulty
var baseTolerance = 200;
var tolerance = Math.max(80, baseTolerance - (currentDifficulty - 1) * 30);
return Math.abs(bottle.x - self.x) <= tolerance;
};
self.calculateAlignment = function () {
if (self.bottles.length < 2) return 1; // Perfect alignment for less than 2 bottles
var totalDeviation = 0;
var centerX = self.x;
for (var i = 0; i < self.bottles.length; i++) {
totalDeviation += Math.abs(self.bottles[i].x - centerX);
}
var averageDeviation = totalDeviation / self.bottles.length;
var maxAllowedDeviation = 100; // Perfect alignment threshold
var alignment = Math.max(0, 1 - averageDeviation / maxAllowedDeviation);
return alignment;
};
self.updateSway = function () {
var alignment = self.calculateAlignment();
self.lastAlignment = alignment;
// Check for tower collapse if very unstable
if (self.bottles.length >= 3 && alignment < 0.3) {
self.collapseTower();
return;
}
// Start swaying if we have 5+ bottles and alignment is poor
if (self.bottles.length >= 5) {
var swayThreshold = 0.7; // Start swaying when alignment drops below 70%
if (alignment < swayThreshold) {
self.isSwaying = true;
self.swayIntensity = Math.max(0.5, 1 - alignment); // More misalignment = more sway
} else {
// Good alignment reduces sway
self.swayIntensity *= 0.95;
if (self.swayIntensity < 0.1) {
self.isSwaying = false;
self.swayIntensity = 0;
}
}
}
if (self.isSwaying) {
// Apply sway physics with increased speed
self.swayVelocity += Math.sin(LK.ticks * 0.15) * 0.0008 * self.swayIntensity;
self.swayVelocity *= 0.98; // Damping
self.swayAngle += self.swayVelocity;
self.swayAngle *= 0.99; // Angle damping
// Apply sway to tower position
var maxSway = 30 * self.swayIntensity;
self.x = gameWidth / 2 + Math.sin(self.swayAngle) * maxSway;
// Apply sway rotation to all bottles
for (var i = 0; i < self.bottles.length; i++) {
var bottle = self.bottles[i];
var swayRotation = Math.sin(self.swayAngle) * 0.05 * self.swayIntensity;
bottle.rotation = swayRotation;
}
}
};
self.collapseTower = function () {
// Animate bottles falling
for (var i = 0; i < self.bottles.length; i++) {
var bottle = self.bottles[i];
var randomX = bottle.x + (Math.random() - 0.5) * 300;
var randomRotation = (Math.random() - 0.5) * Math.PI;
tween(bottle, {
x: randomX,
y: gameHeight + 200,
rotation: randomRotation,
alpha: 0
}, {
duration: 1000 + Math.random() * 500,
easing: tween.easeIn
});
}
// Trigger game over after animation
LK.setTimeout(function () {
gameActive = false;
LK.effects.flashScreen(0xFF0000, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}, 500);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xF5F0E8
});
/****
* Game Code
****/
var gameWidth = 2048;
var gameHeight = 2732;
// Game state variables
var score = 0;
var gameActive = true;
var swingingBottle = null;
var fallingBottle = null;
var towersBuilt = 0;
var currentDifficulty = 1;
// Create tower
var tower = game.addChild(new Tower());
tower.x = gameWidth / 2;
tower.addBase();
// Create score display
var scoreTxt = new Text2('Score: 0', {
size: 80,
fill: 0x8B4513
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Create instruction text
var instructionTxt = new Text2('Touch to release bottle', {
size: 60,
fill: 0xA0522D
});
instructionTxt.anchor.set(0.5, 0);
instructionTxt.y = 120;
LK.gui.top.addChild(instructionTxt);
// Spawn new swinging bottle
function spawnSwingingBottle() {
if (!gameActive || swingingBottle) {
return;
}
swingingBottle = new SwingingBottle();
swingingBottle.x = gameWidth / 2; // Center horizontally
swingingBottle.y = 150; // Position rope anchor point higher
// Apply difficulty scaling
var difficultyMultiplier = 1 + (currentDifficulty - 1) * 0.3;
swingingBottle.swingSpeed *= difficultyMultiplier;
swingingBottle.gravity *= difficultyMultiplier;
swingingBottle.maxSwingAngle = Math.min(Math.PI / 3, Math.PI / 4 * difficultyMultiplier);
// Add random initial swing with increased randomness based on difficulty
var randomness = 0.5 + (currentDifficulty - 1) * 0.2;
swingingBottle.angle = (Math.random() - 0.5) * randomness;
swingingBottle.angularVelocity = (Math.random() - 0.5) * 0.01 * difficultyMultiplier;
game.addChild(swingingBottle);
}
// Release bottle from rope
function releaseBottle() {
if (!swingingBottle || !gameActive) {
return;
}
// Create falling bottle at current swing position
fallingBottle = new FallenBottle();
// Get the actual world position of the swinging bottle
var swingOffset = Math.sin(swingingBottle.angle) * swingingBottle.ropeLength;
fallingBottle.x = swingingBottle.x + swingOffset;
fallingBottle.y = swingingBottle.y + 150; // Account for rope length
game.addChild(fallingBottle);
// Remove swinging bottle
swingingBottle.destroy();
swingingBottle = null;
LK.getSound('placeBottle').play();
}
// Check bottle landing
function checkBottleLanding() {
if (!fallingBottle || fallingBottle.placed) {
return;
}
var expectedLandingHeight = tower.getStackHeight();
// Check if bottle reached landing position
if (fallingBottle.y >= expectedLandingHeight) {
// Check if placement is valid
if (tower.checkValidPlacement(fallingBottle)) {
// Successful placement
tower.addBottle(fallingBottle);
// Create spray effect
var sprayEffect = game.addChild(LK.getAsset('sprayEffect', {
anchorX: 0.5,
anchorY: 0.5
}));
sprayEffect.x = fallingBottle.x;
sprayEffect.y = fallingBottle.y - 100;
sprayEffect.alpha = 0.8;
sprayEffect.scaleX = 0.5;
sprayEffect.scaleY = 0.5;
// Animate spray effect
tween(sprayEffect, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
sprayEffect.destroy();
}
});
fallingBottle = null;
// Calculate score based on alignment
var alignment = tower.calculateAlignment();
var bottleScore = 10; // Base score
if (alignment >= 0.9 && tower.bottles.length === 1) {
bottleScore = 50; // Perfect alignment bonus only for first bottle
}
score += bottleScore;
LK.setScore(score);
scoreTxt.setText('Score: ' + score);
// Check if tower is complete (every 5 bottles = 1 tower)
if (tower.bottles.length % 5 === 0) {
towersBuilt++;
currentDifficulty = Math.min(5, Math.floor(towersBuilt / 2) + 1); // Increase difficulty every 2 towers, max level 5
// Show tower completion effect with different colors based on alignment
var flashColor = alignment >= 0.9 ? 0xFFD700 : 0x90EE90; // Gold for perfect, green for good
LK.effects.flashScreen(flashColor, 500);
}
// Spawn next bottle after delay (shorter delay at higher difficulties)
var spawnDelay = Math.max(500, 1000 - (currentDifficulty - 1) * 100);
LK.setTimeout(function () {
spawnSwingingBottle();
}, spawnDelay);
} else {
// Missed placement - game over
gameActive = false;
LK.effects.flashScreen(0xFF6B6B, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}
}
}
// Game input handling
game.down = function (x, y, obj) {
releaseBottle();
};
// Main game update loop
game.update = function () {
if (!gameActive) {
return;
}
// Update tower sway
tower.updateSway();
// Check bottle landing
checkBottleLanding();
// Check if falling bottle has gone off screen
if (fallingBottle && fallingBottle.y > gameHeight + 100) {
// Missed bottle - game over
gameActive = false;
LK.effects.flashScreen(0xFF6B6B, 1000);
LK.setTimeout(function () {
LK.showGameOver();
}, 1000);
}
};
// Start the game by spawning first swinging bottle
LK.setTimeout(function () {
spawnSwingingBottle();
}, 1000);