/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// PipePair class (top and bottom pipes)
var PipePair = Container.expand(function () {
var self = Container.call(this);
// Gap size and vertical position
self.gap = 600;
self.pipeWidth = 220;
self.pipeHeight = 900;
// Top pipe
self.topPipe = self.attachAsset('pipe', {
anchorX: 0.5,
anchorY: 1.0,
x: 0,
y: 0
});
// Bottom pipe
self.bottomPipe = self.attachAsset('pipe', {
anchorX: 0.5,
anchorY: 0.0,
x: 0,
y: self.gap + self.pipeHeight
});
// Used to check if player has passed this pipe
self.passed = false;
// Set vertical gap position
self.setGapY = function (gapY) {
// gapY is the y of the center of the gap
self.topPipe.y = gapY - self.gap / 2;
self.bottomPipe.y = gapY + self.gap / 2;
};
// Update method
self.update = function () {
self.x -= pipeSpeed;
};
// Get bounds for collision
self.getTopPipeBounds = function () {
return {
x: self.x - self.pipeWidth / 2,
y: self.topPipe.y - self.pipeHeight,
width: self.pipeWidth,
height: self.pipeHeight
};
};
self.getBottomPipeBounds = function () {
return {
x: self.x - self.pipeWidth / 2,
y: self.bottomPipe.y,
width: self.pipeWidth,
height: self.pipeHeight
};
};
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGfx = self.attachAsset('playerSquare', {
anchorX: 0.5,
anchorY: 0.5
});
self.vy = 0; // vertical velocity
self.gravity = 0; // gravity per frame (slower fall)
self.flapStrength = -28; // negative = up (slower flap)
// Flap method
self.flap = function () {
self.vy = self.flapStrength;
// Animate a quick scale for feedback
tween(playerGfx, {
scaleY: 0.7
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
tween(playerGfx, {
scaleY: 1
}, {
duration: 120,
easing: tween.cubicOut
});
}
});
};
// Update method
self.update = function () {
self.vy += self.gravity;
self.y += self.vy;
// Clamp rotation for visual feedback
var maxAngle = Math.PI / 5;
var minAngle = -Math.PI / 6;
var targetRot = Math.max(minAngle, Math.min(maxAngle, self.vy / 60));
playerGfx.rotation = targetRot;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87ceeb // Sky blue
});
/****
* Game Code
****/
// Game constants
// Square player
// Pipe (top and bottom, same asset, different y/flip)
// Ground
var GROUND_HEIGHT = 120;
var PLAYER_START_X = 500;
var PLAYER_START_Y = 1200;
var PIPE_INTERVAL = 900; // px between pipes horizontally
var PIPE_MIN_Y = 500;
var PIPE_MAX_Y = 2732 - GROUND_HEIGHT - 500;
var pipeSpeed = 10;
// Game state
var player;
var pipes = [];
var ground;
var score = 0;
var scoreTxt;
var gameStarted = true;
var gameOver = false;
var lastPipeX = 0;
// Player speed factor: starts at 0.5 (half speed), becomes 1 after first movement
var playerSpeedFactor = 0.5;
var playerHasMoved = false;
// Start menu overlay
var startMenu = new Container();
var startBg = LK.getAsset('ground', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
width: 900,
height: 600
});
startBg.alpha = 0.92;
startMenu.addChild(startBg);
var startText = new Text2("Tap to Start", {
size: 180,
fill: 0xffffff
});
startText.anchor.set(0.5, 0.5);
startText.x = 1024;
startText.y = 1366 - 60;
startMenu.addChild(startText);
var howToText = new Text2("Touch and drag to move the square", {
size: 80,
fill: 0xffffff
});
howToText.anchor.set(0.5, 0.5);
howToText.x = 1024;
howToText.y = 1366 + 120;
startMenu.addChild(howToText);
// Start menu overlay is not shown since game starts immediately
// LK.gui.center.addChild(startMenu);
// Add cloudy background image (behind everything)
var background = LK.getAsset('cloudyBackground', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChild(background);
// Track background offset for parallax effect
var backgroundOffsetY = 0;
// Add ground
ground = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 2732 - GROUND_HEIGHT
});
game.addChild(ground);
// Add player
player = new Player();
player.x = PLAYER_START_X;
player.y = PLAYER_START_Y;
game.addChild(player);
// Add score text
scoreTxt = new Text2('0', {
size: 150,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Helper: reset game state
function resetGame() {
// Remove pipes
for (var i = 0; i < pipes.length; i++) {
pipes[i].destroy();
}
pipes = [];
// Reset player
player.x = PLAYER_START_X;
player.y = PLAYER_START_Y;
player.vy = 0;
// Reset score
score = 0;
scoreTxt.setText(score);
// Reset state
gameStarted = false;
gameOver = false;
lastPipeX = 0;
playerSpeedFactor = 0.5;
playerHasMoved = false;
// Show start menu again
if (startMenu && !startMenu.parent) {
LK.gui.center.addChild(startMenu);
}
}
// Helper: spawn a new pipe pair
function spawnPipePair(x) {
var pipe = new PipePair();
// Randomize gap center
var gapY = PIPE_MIN_Y + Math.floor(Math.random() * (PIPE_MAX_Y - PIPE_MIN_Y));
pipe.x = x;
pipe.setGapY(gapY);
pipes.push(pipe);
game.addChild(pipe);
}
// Start with 3 pipes offscreen to the right
for (var i = 0; i < 3; i++) {
spawnPipePair(2048 + i * PIPE_INTERVAL);
lastPipeX = 2048 + i * PIPE_INTERVAL;
}
// --- Add up and down control buttons ---
var btnUp = LK.getAsset('btnUp', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 2732 - 500
});
var btnDown = LK.getAsset('btnDown', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 2732 - 250
});
btnUp.alpha = 0.85;
btnDown.alpha = 0.85;
game.addChild(btnUp);
game.addChild(btnDown);
// Button state
var btnUpPressed = false;
var btnDownPressed = false;
// Helper to check if a point is inside a button
function isInsideBtn(btn, x, y) {
var bx = btn.x,
by = btn.y;
var bw = btn.width,
bh = btn.height;
return x >= bx - bw / 2 && x <= bx + bw / 2 && y >= by - bh / 2 && y <= by + bh / 2;
}
// Input: touch and drag to control the square directly or press buttons
game.down = function (x, y, obj) {
if (gameOver) {
return;
}
// No need to handle start menu or gameStarted here since game starts immediately
// Check if up or down button pressed
if (isInsideBtn(btnUp, x, y)) {
btnUpPressed = true;
btnDownPressed = false;
return;
}
if (isInsideBtn(btnDown, x, y)) {
btnDownPressed = true;
btnUpPressed = false;
return;
}
// No direct touch control of the square
};
game.move = function (x, y, obj) {
if (gameOver || !gameStarted) return;
// If holding up or down button, ignore drag
if (btnUpPressed || btnDownPressed) return;
if (!game._touching) return;
// No direct touch drag control of the square
};
game.up = function (x, y, obj) {
btnUpPressed = false;
btnDownPressed = false;
game._touching = false;
};
// Main update loop
game.update = function () {
if (gameOver) {
return;
}
// Only update if started
if (gameStarted) {
// Button control: move player up/down if button held
if (btnUpPressed) {
var oldY = player.y;
player.y = Math.max(60, player.y - 22 * playerSpeedFactor);
player.vy = 0;
// Move background at half the speed of the player
var deltaY = player.y - oldY;
backgroundOffsetY += deltaY * 0.5;
// Clamp backgroundOffsetY to [0, 2732 - background.height]
backgroundOffsetY = Math.max(0, Math.min(2732 - background.height, backgroundOffsetY));
background.y = -backgroundOffsetY;
// On first movement, restore speed to 1
if (!playerHasMoved) {
playerSpeedFactor = 1;
playerHasMoved = true;
}
}
if (btnDownPressed) {
var oldY = player.y;
player.y = Math.min(2732 - GROUND_HEIGHT - 60, player.y + 22 * playerSpeedFactor);
player.vy = 0;
// Move background at half the speed of the player
var deltaY = player.y - oldY;
backgroundOffsetY += deltaY * 0.5;
// Clamp backgroundOffsetY to [0, 2732 - background.height]
backgroundOffsetY = Math.max(0, Math.min(2732 - background.height, backgroundOffsetY));
background.y = -backgroundOffsetY;
// On first movement, restore speed to 1
if (!playerHasMoved) {
playerSpeedFactor = 1;
playerHasMoved = true;
}
}
player.update();
// Move pipes and check for offscreen
for (var i = pipes.length - 1; i >= 0; i--) {
var pipe = pipes[i];
pipe.update();
// Remove pipes that are offscreen left
if (pipe.x < -pipe.pipeWidth) {
pipe.destroy();
pipes.splice(i, 1);
continue;
}
// Check for passing pipe (score)
if (!pipe.passed && pipe.x + pipe.pipeWidth / 2 < player.x) {
pipe.passed = true;
score += 1;
scoreTxt.setText(score);
}
}
// Spawn new pipes as needed
if (pipes.length === 0 || lastPipeX - pipes[pipes.length - 1].x >= PIPE_INTERVAL) {
lastPipeX += PIPE_INTERVAL;
spawnPipePair(lastPipeX);
}
// Collision detection: pipes
for (var i = 0; i < pipes.length; i++) {
var pipe = pipes[i];
// Top pipe
var topB = pipe.getTopPipeBounds();
if (rectsIntersect(player, topB)) {
endGame();
return;
}
// Bottom pipe
var botB = pipe.getBottomPipeBounds();
if (rectsIntersect(player, botB)) {
endGame();
return;
}
}
// Collision detection: ground
if (player.y + 60 > 2732 - GROUND_HEIGHT) {
player.y = 2732 - GROUND_HEIGHT - 60;
endGame();
return;
}
// Collision detection: ceiling
if (player.y - 60 < 0) {
player.y = 60;
player.vy = 0;
}
}
};
// Rectangle intersection helper (player is a square, pipes are rectangles)
function rectsIntersect(playerObj, rect) {
// Player bounds
var px = playerObj.x - 60;
var py = playerObj.y - 60;
var pw = 120;
var ph = 120;
// Rect bounds
var rx = rect.x;
var ry = rect.y;
var rw = rect.width;
var rh = rect.height;
// Check overlap
return !(px + pw < rx || px > rx + rw || py + ph < ry || py > ry + rh);
}
// End game
function endGame() {
gameOver = true;
// Flash red
LK.effects.flashScreen(0xff0000, 800);
// Show game over (auto resets game)
LK.setScore(score);
LK.showGameOver();
}
// Reset game on game over
LK.on('gameover', function () {
resetGame();
});
// Also reset on you win (not used, but for completeness)
LK.on('youwin', function () {
resetGame();
}); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// PipePair class (top and bottom pipes)
var PipePair = Container.expand(function () {
var self = Container.call(this);
// Gap size and vertical position
self.gap = 600;
self.pipeWidth = 220;
self.pipeHeight = 900;
// Top pipe
self.topPipe = self.attachAsset('pipe', {
anchorX: 0.5,
anchorY: 1.0,
x: 0,
y: 0
});
// Bottom pipe
self.bottomPipe = self.attachAsset('pipe', {
anchorX: 0.5,
anchorY: 0.0,
x: 0,
y: self.gap + self.pipeHeight
});
// Used to check if player has passed this pipe
self.passed = false;
// Set vertical gap position
self.setGapY = function (gapY) {
// gapY is the y of the center of the gap
self.topPipe.y = gapY - self.gap / 2;
self.bottomPipe.y = gapY + self.gap / 2;
};
// Update method
self.update = function () {
self.x -= pipeSpeed;
};
// Get bounds for collision
self.getTopPipeBounds = function () {
return {
x: self.x - self.pipeWidth / 2,
y: self.topPipe.y - self.pipeHeight,
width: self.pipeWidth,
height: self.pipeHeight
};
};
self.getBottomPipeBounds = function () {
return {
x: self.x - self.pipeWidth / 2,
y: self.bottomPipe.y,
width: self.pipeWidth,
height: self.pipeHeight
};
};
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGfx = self.attachAsset('playerSquare', {
anchorX: 0.5,
anchorY: 0.5
});
self.vy = 0; // vertical velocity
self.gravity = 0; // gravity per frame (slower fall)
self.flapStrength = -28; // negative = up (slower flap)
// Flap method
self.flap = function () {
self.vy = self.flapStrength;
// Animate a quick scale for feedback
tween(playerGfx, {
scaleY: 0.7
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
tween(playerGfx, {
scaleY: 1
}, {
duration: 120,
easing: tween.cubicOut
});
}
});
};
// Update method
self.update = function () {
self.vy += self.gravity;
self.y += self.vy;
// Clamp rotation for visual feedback
var maxAngle = Math.PI / 5;
var minAngle = -Math.PI / 6;
var targetRot = Math.max(minAngle, Math.min(maxAngle, self.vy / 60));
playerGfx.rotation = targetRot;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87ceeb // Sky blue
});
/****
* Game Code
****/
// Game constants
// Square player
// Pipe (top and bottom, same asset, different y/flip)
// Ground
var GROUND_HEIGHT = 120;
var PLAYER_START_X = 500;
var PLAYER_START_Y = 1200;
var PIPE_INTERVAL = 900; // px between pipes horizontally
var PIPE_MIN_Y = 500;
var PIPE_MAX_Y = 2732 - GROUND_HEIGHT - 500;
var pipeSpeed = 10;
// Game state
var player;
var pipes = [];
var ground;
var score = 0;
var scoreTxt;
var gameStarted = true;
var gameOver = false;
var lastPipeX = 0;
// Player speed factor: starts at 0.5 (half speed), becomes 1 after first movement
var playerSpeedFactor = 0.5;
var playerHasMoved = false;
// Start menu overlay
var startMenu = new Container();
var startBg = LK.getAsset('ground', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
width: 900,
height: 600
});
startBg.alpha = 0.92;
startMenu.addChild(startBg);
var startText = new Text2("Tap to Start", {
size: 180,
fill: 0xffffff
});
startText.anchor.set(0.5, 0.5);
startText.x = 1024;
startText.y = 1366 - 60;
startMenu.addChild(startText);
var howToText = new Text2("Touch and drag to move the square", {
size: 80,
fill: 0xffffff
});
howToText.anchor.set(0.5, 0.5);
howToText.x = 1024;
howToText.y = 1366 + 120;
startMenu.addChild(howToText);
// Start menu overlay is not shown since game starts immediately
// LK.gui.center.addChild(startMenu);
// Add cloudy background image (behind everything)
var background = LK.getAsset('cloudyBackground', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChild(background);
// Track background offset for parallax effect
var backgroundOffsetY = 0;
// Add ground
ground = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 2732 - GROUND_HEIGHT
});
game.addChild(ground);
// Add player
player = new Player();
player.x = PLAYER_START_X;
player.y = PLAYER_START_Y;
game.addChild(player);
// Add score text
scoreTxt = new Text2('0', {
size: 150,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Helper: reset game state
function resetGame() {
// Remove pipes
for (var i = 0; i < pipes.length; i++) {
pipes[i].destroy();
}
pipes = [];
// Reset player
player.x = PLAYER_START_X;
player.y = PLAYER_START_Y;
player.vy = 0;
// Reset score
score = 0;
scoreTxt.setText(score);
// Reset state
gameStarted = false;
gameOver = false;
lastPipeX = 0;
playerSpeedFactor = 0.5;
playerHasMoved = false;
// Show start menu again
if (startMenu && !startMenu.parent) {
LK.gui.center.addChild(startMenu);
}
}
// Helper: spawn a new pipe pair
function spawnPipePair(x) {
var pipe = new PipePair();
// Randomize gap center
var gapY = PIPE_MIN_Y + Math.floor(Math.random() * (PIPE_MAX_Y - PIPE_MIN_Y));
pipe.x = x;
pipe.setGapY(gapY);
pipes.push(pipe);
game.addChild(pipe);
}
// Start with 3 pipes offscreen to the right
for (var i = 0; i < 3; i++) {
spawnPipePair(2048 + i * PIPE_INTERVAL);
lastPipeX = 2048 + i * PIPE_INTERVAL;
}
// --- Add up and down control buttons ---
var btnUp = LK.getAsset('btnUp', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 2732 - 500
});
var btnDown = LK.getAsset('btnDown', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 200,
y: 2732 - 250
});
btnUp.alpha = 0.85;
btnDown.alpha = 0.85;
game.addChild(btnUp);
game.addChild(btnDown);
// Button state
var btnUpPressed = false;
var btnDownPressed = false;
// Helper to check if a point is inside a button
function isInsideBtn(btn, x, y) {
var bx = btn.x,
by = btn.y;
var bw = btn.width,
bh = btn.height;
return x >= bx - bw / 2 && x <= bx + bw / 2 && y >= by - bh / 2 && y <= by + bh / 2;
}
// Input: touch and drag to control the square directly or press buttons
game.down = function (x, y, obj) {
if (gameOver) {
return;
}
// No need to handle start menu or gameStarted here since game starts immediately
// Check if up or down button pressed
if (isInsideBtn(btnUp, x, y)) {
btnUpPressed = true;
btnDownPressed = false;
return;
}
if (isInsideBtn(btnDown, x, y)) {
btnDownPressed = true;
btnUpPressed = false;
return;
}
// No direct touch control of the square
};
game.move = function (x, y, obj) {
if (gameOver || !gameStarted) return;
// If holding up or down button, ignore drag
if (btnUpPressed || btnDownPressed) return;
if (!game._touching) return;
// No direct touch drag control of the square
};
game.up = function (x, y, obj) {
btnUpPressed = false;
btnDownPressed = false;
game._touching = false;
};
// Main update loop
game.update = function () {
if (gameOver) {
return;
}
// Only update if started
if (gameStarted) {
// Button control: move player up/down if button held
if (btnUpPressed) {
var oldY = player.y;
player.y = Math.max(60, player.y - 22 * playerSpeedFactor);
player.vy = 0;
// Move background at half the speed of the player
var deltaY = player.y - oldY;
backgroundOffsetY += deltaY * 0.5;
// Clamp backgroundOffsetY to [0, 2732 - background.height]
backgroundOffsetY = Math.max(0, Math.min(2732 - background.height, backgroundOffsetY));
background.y = -backgroundOffsetY;
// On first movement, restore speed to 1
if (!playerHasMoved) {
playerSpeedFactor = 1;
playerHasMoved = true;
}
}
if (btnDownPressed) {
var oldY = player.y;
player.y = Math.min(2732 - GROUND_HEIGHT - 60, player.y + 22 * playerSpeedFactor);
player.vy = 0;
// Move background at half the speed of the player
var deltaY = player.y - oldY;
backgroundOffsetY += deltaY * 0.5;
// Clamp backgroundOffsetY to [0, 2732 - background.height]
backgroundOffsetY = Math.max(0, Math.min(2732 - background.height, backgroundOffsetY));
background.y = -backgroundOffsetY;
// On first movement, restore speed to 1
if (!playerHasMoved) {
playerSpeedFactor = 1;
playerHasMoved = true;
}
}
player.update();
// Move pipes and check for offscreen
for (var i = pipes.length - 1; i >= 0; i--) {
var pipe = pipes[i];
pipe.update();
// Remove pipes that are offscreen left
if (pipe.x < -pipe.pipeWidth) {
pipe.destroy();
pipes.splice(i, 1);
continue;
}
// Check for passing pipe (score)
if (!pipe.passed && pipe.x + pipe.pipeWidth / 2 < player.x) {
pipe.passed = true;
score += 1;
scoreTxt.setText(score);
}
}
// Spawn new pipes as needed
if (pipes.length === 0 || lastPipeX - pipes[pipes.length - 1].x >= PIPE_INTERVAL) {
lastPipeX += PIPE_INTERVAL;
spawnPipePair(lastPipeX);
}
// Collision detection: pipes
for (var i = 0; i < pipes.length; i++) {
var pipe = pipes[i];
// Top pipe
var topB = pipe.getTopPipeBounds();
if (rectsIntersect(player, topB)) {
endGame();
return;
}
// Bottom pipe
var botB = pipe.getBottomPipeBounds();
if (rectsIntersect(player, botB)) {
endGame();
return;
}
}
// Collision detection: ground
if (player.y + 60 > 2732 - GROUND_HEIGHT) {
player.y = 2732 - GROUND_HEIGHT - 60;
endGame();
return;
}
// Collision detection: ceiling
if (player.y - 60 < 0) {
player.y = 60;
player.vy = 0;
}
}
};
// Rectangle intersection helper (player is a square, pipes are rectangles)
function rectsIntersect(playerObj, rect) {
// Player bounds
var px = playerObj.x - 60;
var py = playerObj.y - 60;
var pw = 120;
var ph = 120;
// Rect bounds
var rx = rect.x;
var ry = rect.y;
var rw = rect.width;
var rh = rect.height;
// Check overlap
return !(px + pw < rx || px > rx + rw || py + ph < ry || py > ry + rh);
}
// End game
function endGame() {
gameOver = true;
// Flash red
LK.effects.flashScreen(0xff0000, 800);
// Show game over (auto resets game)
LK.setScore(score);
LK.showGameOver();
}
// Reset game on game over
LK.on('gameover', function () {
resetGame();
});
// Also reset on you win (not used, but for completeness)
LK.on('youwin', function () {
resetGame();
});