/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Drawn Line class (user drawn) var DrawnLine = Container.expand(function () { var self = Container.call(this); // Line endpoints in game coordinates self.x1 = 0; self.y1 = 0; self.x2 = 0; self.y2 = 0; // For rendering, we use a box asset and stretch/rotate it var lineSprite = self.attachAsset('drawnLine', { anchorX: 0, anchorY: 0.5 }); // Set endpoints and update visual self.setEndpoints = function (x1, y1, x2, y2) { self.x1 = x1; self.y1 = y1; self.x2 = x2; self.y2 = y2; // Position at (x1, y1) self.x = x1; self.y = y1; // Calculate length and angle var dx = x2 - x1; var dy = y2 - y1; var len = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx); lineSprite.width = len; lineSprite.height = 16; self.rotation = angle; }; // Collision detection with droplet (circle-line segment) self.collidesWithDroplet = function (droplet) { // Closest point on line segment to droplet center var x1 = self.x1, y1 = self.y1, x2 = self.x2, y2 = self.y2; var px = droplet.x, py = droplet.y; var dx = x2 - x1, dy = y2 - y1; var lengthSq = dx * dx + dy * dy; var t = ((px - x1) * dx + (py - y1) * dy) / (lengthSq || 1); t = Math.max(0, Math.min(1, t)); var closestX = x1 + t * dx; var closestY = y1 + t * dy; var distSq = (px - closestX) * (px - closestX) + (py - closestY) * (py - closestY); return distSq <= (droplet.radius + 8) * (droplet.radius + 8); }; // Get normal vector of the line (unit vector) self.getNormal = function () { var dx = self.x2 - self.x1; var dy = self.y2 - self.y1; var len = Math.sqrt(dx * dx + dy * dy) || 1; // Perpendicular (normal) vector return { x: -dy / len, y: dx / len }; }; return self; }); // Water Droplet class var Droplet = Container.expand(function () { var self = Container.call(this); var dropletSprite = self.attachAsset('droplet', { anchorX: 0.5, anchorY: 0.5 }); // Physics properties self.vx = 0; self.vy = 0; self.radius = dropletSprite.width / 2; self.caught = false; // If already scored // Update method called every tick self.update = function () { // Gravity self.vy += 0.4 * dropletGravityMultiplier; self.x += self.vx; self.y += self.vy; // Collide with drawn lines for (var i = 0; i < drawnLines.length; i++) { var line = drawnLines[i]; if (line.collidesWithDroplet(self)) { // Reflect velocity based on line angle var normal = line.getNormal(); // Project velocity onto normal var dot = self.vx * normal.x + self.vy * normal.y; self.vx = self.vx - 2 * dot * normal.x; self.vy = self.vy - 2 * dot * normal.y; // Dampen velocity a bit self.vx *= 0.7; self.vy *= 0.7; // Move droplet slightly away from line to prevent sticking self.x += normal.x * 4; self.y += normal.y * 4; } } // Collide with obstacles for (var i = 0; i < obstacles.length; i++) { var obs = obstacles[i]; if (self.intersects(obs)) { // Simple bounce: reverse vy, dampen self.vy = -Math.abs(self.vy) * 0.6; // Nudge out of obstacle if (self.y < obs.y) { self.y = obs.y - obs.height / 2 - self.radius - 2; } else { self.y = obs.y + obs.height / 2 + self.radius + 2; } } } // Collide with bucket if (!self.caught && self.intersects(bucket)) { // Check if inside bucket opening (not just touching sides) var bucketTop = bucket.y - bucket.height / 2; if (self.y + self.radius > bucketTop) { self.caught = true; // Play waterfall sound LK.getSound('waterfall').play(); LK.setScore(LK.getScore() + 1); scoreText.setText(LK.getScore()); // Animate droplet into bucket tween(self, { alpha: 0, y: bucket.y + bucket.height / 2 }, { duration: 400, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); removeDroplet(self); } }); return; } } // Out of bounds (left/right/bottom) if (self.y - self.radius > 2732 || self.x + self.radius < 0 || self.x - self.radius > 2048) { if (!self.caught) { lostDroplets++; updateLostText(); if (lostDroplets >= maxLostDroplets) { LK.showGameOver(); } } self.destroy(); removeDroplet(self); } }; return self; }); // Obstacle class var Obstacle = Container.expand(function () { var self = Container.call(this); var obsSprite = self.attachAsset('obstacle', { anchorX: 0.5, anchorY: 0.5 }); return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ // No title, no description // Always backgroundColor is black backgroundColor: 0x000000 }); /**** * Game Code ****/ // Game variables // Droplet: blue ellipse // Bucket: gray box // Obstacle: dark box // Line: blue box (for drawn lines) var droplets = []; var drawnLines = []; var obstacles = []; var dropletSpawnTimer = 0; var dropletSpawnInterval = 60; // ticks var dropletGravityMultiplier = 1; var lostDroplets = 0; var maxLostDroplets = 10; var currentLevel = 1; var isDrawing = false; var drawStartX = 0, drawStartY = 0; var drawLinePreview = null; var bucket = null; // Score display var scoreText = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreText.anchor.set(0.5, 0); LK.gui.top.addChild(scoreText); // Lost droplets display var lostText = new Text2('Lost: 0/10', { size: 60, fill: 0xFF6666 }); lostText.anchor.set(1, 0); LK.gui.topRight.addChild(lostText); function updateLostText() { lostText.setText('Lost: ' + lostDroplets + '/' + maxLostDroplets); } // Bucket setup bucket = new Container(); var bucketSprite = bucket.attachAsset('bucket', { anchorX: 0.5, anchorY: 0.5 }); bucket.width = bucketSprite.width; bucket.height = bucketSprite.height; bucket.x = 2048 / 2; bucket.y = 2732 - 180; game.addChild(bucket); // Obstacles setup (for level 1, one obstacle; more in higher levels) function setupObstacles(level) { // Remove old for (var i = 0; i < obstacles.length; i++) { obstacles[i].destroy(); } obstacles = []; if (level === 1) { var obs = new Obstacle(); obs.x = 2048 / 2; obs.y = 1200; game.addChild(obs); obstacles.push(obs); } else if (level === 2) { var obs1 = new Obstacle(); obs1.x = 2048 / 2 - 300; obs1.y = 1100; game.addChild(obs1); obstacles.push(obs1); var obs2 = new Obstacle(); obs2.x = 2048 / 2 + 300; obs2.y = 1500; game.addChild(obs2); obstacles.push(obs2); } else if (level >= 3) { for (var i = 0; i < 3; i++) { var obs = new Obstacle(); obs.x = 600 + i * 400; obs.y = 900 + i * 400; game.addChild(obs); obstacles.push(obs); } } } setupObstacles(currentLevel); // Remove droplet from array function removeDroplet(droplet) { for (var i = droplets.length - 1; i >= 0; i--) { if (droplets[i] === droplet) { droplets.splice(i, 1); break; } } } // Draw line preview (while dragging) function showDrawPreview(x1, y1, x2, y2) { if (!drawLinePreview) { drawLinePreview = new DrawnLine(); game.addChild(drawLinePreview); } drawLinePreview.setEndpoints(x1, y1, x2, y2); drawLinePreview.alpha = 0.5; } function hideDrawPreview() { if (drawLinePreview) { drawLinePreview.destroy(); drawLinePreview = null; } } // Drawing lines: only allow a max number of lines at once var maxDrawnLines = 4; function addDrawnLine(x1, y1, x2, y2) { if (drawnLines.length >= maxDrawnLines) { // Remove oldest var old = drawnLines.shift(); old.destroy(); } var line = new DrawnLine(); line.setEndpoints(x1, y1, x2, y2); game.addChild(line); drawnLines.push(line); } // Touch/mouse events for drawing lines game.down = function (x, y, obj) { // Only allow drawing in lower 90% of screen (avoid top menu) if (y < 120) return; isDrawing = true; drawStartX = x; drawStartY = y; showDrawPreview(drawStartX, drawStartY, x, y); }; game.move = function (x, y, obj) { if (isDrawing) { showDrawPreview(drawStartX, drawStartY, x, y); } }; game.up = function (x, y, obj) { if (isDrawing) { // Only draw if line is long enough var dx = x - drawStartX, dy = y - drawStartY; var len = Math.sqrt(dx * dx + dy * dy); if (len > 80) { addDrawnLine(drawStartX, drawStartY, x, y); } hideDrawPreview(); isDrawing = false; } }; // Droplet spawning function spawnDroplet() { var droplet = new Droplet(); // Random x, avoid spawning at extreme edges droplet.x = 180 + Math.random() * (2048 - 360); droplet.y = -40; // Small random initial vx droplet.vx = (Math.random() - 0.5) * 2; droplet.vy = 2 + Math.random() * 2 + dropletGravityMultiplier; droplets.push(droplet); game.addChild(droplet); } // Level progression function nextLevel() { currentLevel++; if (currentLevel > 5) currentLevel = 5; dropletGravityMultiplier = 1 + 0.2 * (currentLevel - 1); dropletSpawnInterval = Math.max(30, 60 - 8 * (currentLevel - 1)); setupObstacles(currentLevel); } // Main game update game.update = function () { // Spawn droplets dropletSpawnTimer++; if (dropletSpawnTimer >= dropletSpawnInterval) { dropletSpawnTimer = 0; spawnDroplet(); } // Update all droplets for (var i = 0; i < droplets.length; i++) { if (droplets[i].update) droplets[i].update(); } // Level up every 10 points var score = LK.getScore(); if (score > 0 && score % 10 === 0 && currentLevel < 5) { nextLevel(); } }; // Reset game state on new game LK.on('gameStart', function () { LK.playMusic('pixelation'); // Remove all droplets for (var i = 0; i < droplets.length; i++) { droplets[i].destroy(); } droplets = []; // Remove all lines for (var i = 0; i < drawnLines.length; i++) { drawnLines[i].destroy(); } drawnLines = []; // Remove obstacles for (var i = 0; i < obstacles.length; i++) { obstacles[i].destroy(); } obstacles = []; // Reset variables dropletSpawnTimer = 0; dropletSpawnInterval = 60; dropletGravityMultiplier = 1; lostDroplets = 0; maxLostDroplets = 10; currentLevel = 1; updateLostText(); scoreText.setText('0'); setupObstacles(currentLevel); });
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Drawn Line class (user drawn)
var DrawnLine = Container.expand(function () {
var self = Container.call(this);
// Line endpoints in game coordinates
self.x1 = 0;
self.y1 = 0;
self.x2 = 0;
self.y2 = 0;
// For rendering, we use a box asset and stretch/rotate it
var lineSprite = self.attachAsset('drawnLine', {
anchorX: 0,
anchorY: 0.5
});
// Set endpoints and update visual
self.setEndpoints = function (x1, y1, x2, y2) {
self.x1 = x1;
self.y1 = y1;
self.x2 = x2;
self.y2 = y2;
// Position at (x1, y1)
self.x = x1;
self.y = y1;
// Calculate length and angle
var dx = x2 - x1;
var dy = y2 - y1;
var len = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
lineSprite.width = len;
lineSprite.height = 16;
self.rotation = angle;
};
// Collision detection with droplet (circle-line segment)
self.collidesWithDroplet = function (droplet) {
// Closest point on line segment to droplet center
var x1 = self.x1,
y1 = self.y1,
x2 = self.x2,
y2 = self.y2;
var px = droplet.x,
py = droplet.y;
var dx = x2 - x1,
dy = y2 - y1;
var lengthSq = dx * dx + dy * dy;
var t = ((px - x1) * dx + (py - y1) * dy) / (lengthSq || 1);
t = Math.max(0, Math.min(1, t));
var closestX = x1 + t * dx;
var closestY = y1 + t * dy;
var distSq = (px - closestX) * (px - closestX) + (py - closestY) * (py - closestY);
return distSq <= (droplet.radius + 8) * (droplet.radius + 8);
};
// Get normal vector of the line (unit vector)
self.getNormal = function () {
var dx = self.x2 - self.x1;
var dy = self.y2 - self.y1;
var len = Math.sqrt(dx * dx + dy * dy) || 1;
// Perpendicular (normal) vector
return {
x: -dy / len,
y: dx / len
};
};
return self;
});
// Water Droplet class
var Droplet = Container.expand(function () {
var self = Container.call(this);
var dropletSprite = self.attachAsset('droplet', {
anchorX: 0.5,
anchorY: 0.5
});
// Physics properties
self.vx = 0;
self.vy = 0;
self.radius = dropletSprite.width / 2;
self.caught = false; // If already scored
// Update method called every tick
self.update = function () {
// Gravity
self.vy += 0.4 * dropletGravityMultiplier;
self.x += self.vx;
self.y += self.vy;
// Collide with drawn lines
for (var i = 0; i < drawnLines.length; i++) {
var line = drawnLines[i];
if (line.collidesWithDroplet(self)) {
// Reflect velocity based on line angle
var normal = line.getNormal();
// Project velocity onto normal
var dot = self.vx * normal.x + self.vy * normal.y;
self.vx = self.vx - 2 * dot * normal.x;
self.vy = self.vy - 2 * dot * normal.y;
// Dampen velocity a bit
self.vx *= 0.7;
self.vy *= 0.7;
// Move droplet slightly away from line to prevent sticking
self.x += normal.x * 4;
self.y += normal.y * 4;
}
}
// Collide with obstacles
for (var i = 0; i < obstacles.length; i++) {
var obs = obstacles[i];
if (self.intersects(obs)) {
// Simple bounce: reverse vy, dampen
self.vy = -Math.abs(self.vy) * 0.6;
// Nudge out of obstacle
if (self.y < obs.y) {
self.y = obs.y - obs.height / 2 - self.radius - 2;
} else {
self.y = obs.y + obs.height / 2 + self.radius + 2;
}
}
}
// Collide with bucket
if (!self.caught && self.intersects(bucket)) {
// Check if inside bucket opening (not just touching sides)
var bucketTop = bucket.y - bucket.height / 2;
if (self.y + self.radius > bucketTop) {
self.caught = true;
// Play waterfall sound
LK.getSound('waterfall').play();
LK.setScore(LK.getScore() + 1);
scoreText.setText(LK.getScore());
// Animate droplet into bucket
tween(self, {
alpha: 0,
y: bucket.y + bucket.height / 2
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
removeDroplet(self);
}
});
return;
}
}
// Out of bounds (left/right/bottom)
if (self.y - self.radius > 2732 || self.x + self.radius < 0 || self.x - self.radius > 2048) {
if (!self.caught) {
lostDroplets++;
updateLostText();
if (lostDroplets >= maxLostDroplets) {
LK.showGameOver();
}
}
self.destroy();
removeDroplet(self);
}
};
return self;
});
// Obstacle class
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obsSprite = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
// No title, no description
// Always backgroundColor is black
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game variables
// Droplet: blue ellipse
// Bucket: gray box
// Obstacle: dark box
// Line: blue box (for drawn lines)
var droplets = [];
var drawnLines = [];
var obstacles = [];
var dropletSpawnTimer = 0;
var dropletSpawnInterval = 60; // ticks
var dropletGravityMultiplier = 1;
var lostDroplets = 0;
var maxLostDroplets = 10;
var currentLevel = 1;
var isDrawing = false;
var drawStartX = 0,
drawStartY = 0;
var drawLinePreview = null;
var bucket = null;
// Score display
var scoreText = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
// Lost droplets display
var lostText = new Text2('Lost: 0/10', {
size: 60,
fill: 0xFF6666
});
lostText.anchor.set(1, 0);
LK.gui.topRight.addChild(lostText);
function updateLostText() {
lostText.setText('Lost: ' + lostDroplets + '/' + maxLostDroplets);
}
// Bucket setup
bucket = new Container();
var bucketSprite = bucket.attachAsset('bucket', {
anchorX: 0.5,
anchorY: 0.5
});
bucket.width = bucketSprite.width;
bucket.height = bucketSprite.height;
bucket.x = 2048 / 2;
bucket.y = 2732 - 180;
game.addChild(bucket);
// Obstacles setup (for level 1, one obstacle; more in higher levels)
function setupObstacles(level) {
// Remove old
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
if (level === 1) {
var obs = new Obstacle();
obs.x = 2048 / 2;
obs.y = 1200;
game.addChild(obs);
obstacles.push(obs);
} else if (level === 2) {
var obs1 = new Obstacle();
obs1.x = 2048 / 2 - 300;
obs1.y = 1100;
game.addChild(obs1);
obstacles.push(obs1);
var obs2 = new Obstacle();
obs2.x = 2048 / 2 + 300;
obs2.y = 1500;
game.addChild(obs2);
obstacles.push(obs2);
} else if (level >= 3) {
for (var i = 0; i < 3; i++) {
var obs = new Obstacle();
obs.x = 600 + i * 400;
obs.y = 900 + i * 400;
game.addChild(obs);
obstacles.push(obs);
}
}
}
setupObstacles(currentLevel);
// Remove droplet from array
function removeDroplet(droplet) {
for (var i = droplets.length - 1; i >= 0; i--) {
if (droplets[i] === droplet) {
droplets.splice(i, 1);
break;
}
}
}
// Draw line preview (while dragging)
function showDrawPreview(x1, y1, x2, y2) {
if (!drawLinePreview) {
drawLinePreview = new DrawnLine();
game.addChild(drawLinePreview);
}
drawLinePreview.setEndpoints(x1, y1, x2, y2);
drawLinePreview.alpha = 0.5;
}
function hideDrawPreview() {
if (drawLinePreview) {
drawLinePreview.destroy();
drawLinePreview = null;
}
}
// Drawing lines: only allow a max number of lines at once
var maxDrawnLines = 4;
function addDrawnLine(x1, y1, x2, y2) {
if (drawnLines.length >= maxDrawnLines) {
// Remove oldest
var old = drawnLines.shift();
old.destroy();
}
var line = new DrawnLine();
line.setEndpoints(x1, y1, x2, y2);
game.addChild(line);
drawnLines.push(line);
}
// Touch/mouse events for drawing lines
game.down = function (x, y, obj) {
// Only allow drawing in lower 90% of screen (avoid top menu)
if (y < 120) return;
isDrawing = true;
drawStartX = x;
drawStartY = y;
showDrawPreview(drawStartX, drawStartY, x, y);
};
game.move = function (x, y, obj) {
if (isDrawing) {
showDrawPreview(drawStartX, drawStartY, x, y);
}
};
game.up = function (x, y, obj) {
if (isDrawing) {
// Only draw if line is long enough
var dx = x - drawStartX,
dy = y - drawStartY;
var len = Math.sqrt(dx * dx + dy * dy);
if (len > 80) {
addDrawnLine(drawStartX, drawStartY, x, y);
}
hideDrawPreview();
isDrawing = false;
}
};
// Droplet spawning
function spawnDroplet() {
var droplet = new Droplet();
// Random x, avoid spawning at extreme edges
droplet.x = 180 + Math.random() * (2048 - 360);
droplet.y = -40;
// Small random initial vx
droplet.vx = (Math.random() - 0.5) * 2;
droplet.vy = 2 + Math.random() * 2 + dropletGravityMultiplier;
droplets.push(droplet);
game.addChild(droplet);
}
// Level progression
function nextLevel() {
currentLevel++;
if (currentLevel > 5) currentLevel = 5;
dropletGravityMultiplier = 1 + 0.2 * (currentLevel - 1);
dropletSpawnInterval = Math.max(30, 60 - 8 * (currentLevel - 1));
setupObstacles(currentLevel);
}
// Main game update
game.update = function () {
// Spawn droplets
dropletSpawnTimer++;
if (dropletSpawnTimer >= dropletSpawnInterval) {
dropletSpawnTimer = 0;
spawnDroplet();
}
// Update all droplets
for (var i = 0; i < droplets.length; i++) {
if (droplets[i].update) droplets[i].update();
}
// Level up every 10 points
var score = LK.getScore();
if (score > 0 && score % 10 === 0 && currentLevel < 5) {
nextLevel();
}
};
// Reset game state on new game
LK.on('gameStart', function () {
LK.playMusic('pixelation');
// Remove all droplets
for (var i = 0; i < droplets.length; i++) {
droplets[i].destroy();
}
droplets = [];
// Remove all lines
for (var i = 0; i < drawnLines.length; i++) {
drawnLines[i].destroy();
}
drawnLines = [];
// Remove obstacles
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
obstacles = [];
// Reset variables
dropletSpawnTimer = 0;
dropletSpawnInterval = 60;
dropletGravityMultiplier = 1;
lostDroplets = 0;
maxLostDroplets = 10;
currentLevel = 1;
updateLostText();
scoreText.setText('0');
setupObstacles(currentLevel);
});