/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
bestScore: 0
});
/****
* Classes
****/
// Bird class
var Bird = Container.expand(function () {
var self = Container.call(this);
// Attach yellow ellipse as bird (now a shape, not an image)
var birdAsset = self.attachAsset('bird', {
anchorX: 0.5,
anchorY: 0.5
});
// Physics
self.vy = 0;
self.gravity = 0.45; // px per frame^2 (even slower fall)
self.jumpStrength = -14; // px per frame (even lower jump, less height per tap)
// Bird size for collision
self.radius = birdAsset.width * 0.45;
// Flap animation (squash/stretch)
self.flap = function () {
// Animate scale for a quick squash
tween.stop(birdAsset, {
scaleY: true,
scaleX: true
});
birdAsset.scaleY = 1.0;
birdAsset.scaleX = 1.0;
tween(birdAsset, {
scaleY: 0.7,
scaleX: 1.2
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
tween(birdAsset, {
scaleY: 1.0,
scaleX: 1.0
}, {
duration: 120,
easing: tween.cubicIn
});
}
});
};
// Bird physics update
self.update = function () {
self.vy += self.gravity;
self.y += self.vy;
// Only move forward (x) if the player has tapped (i.e. on jump)
if (gameState === 'play' && self._moveForward) {
self.x += 3; // Move forward even more slowly per jump
self._moveForward = false;
}
// Clamp rotation based on vy
var maxAngle = Math.PI / 4;
var minAngle = -Math.PI / 6;
var t = Math.max(-20, Math.min(20, self.vy)) / 20;
birdAsset.rotation = minAngle + (maxAngle - minAngle) * ((t + 1) / 2);
};
// Reset bird state
self.reset = function (x, y) {
self.x = x;
self.y = y;
self.vy = 0;
birdAsset.rotation = 0;
birdAsset.scaleX = 1.0;
birdAsset.scaleY = 1.0;
};
return self;
});
// PipePair class (top and bottom pipes)
var PipePair = Container.expand(function () {
var self = Container.call(this);
// Pipe config
self.pipeWidth = 220;
self.gapHeight = 520;
self.speed = 12; // px per frame
// Top pipe
var topPipe = self.attachAsset('pipe', {
anchorX: 0,
anchorY: 1,
width: self.pipeWidth,
height: 900,
y: 0
});
// Bottom pipe
var bottomPipe = self.attachAsset('pipe', {
anchorX: 0,
anchorY: 0,
width: self.pipeWidth,
height: 900,
y: 0
});
// For collision
self.topRect = new Rectangle();
self.bottomRect = new Rectangle();
// Set vertical gap position
self.setGap = function (gapY) {
// gapY is the center of the gap
var pipeTop = gapY - self.gapHeight / 2;
var pipeBottom = gapY + self.gapHeight / 2;
topPipe.height = Math.max(80, pipeTop);
topPipe.y = pipeTop;
bottomPipe.height = Math.max(80, 2732 - pipeBottom - 220);
bottomPipe.y = pipeBottom;
};
// Update position
self.update = function () {
self.x -= self.speed;
};
// Get rectangles for collision
self.getRects = function () {
// Top pipe
self.topRect.x = self.x;
self.topRect.y = 0;
self.topRect.width = self.pipeWidth;
self.topRect.height = topPipe.height;
// Bottom pipe
self.bottomRect.x = self.x;
self.bottomRect.y = bottomPipe.y;
self.bottomRect.width = self.pipeWidth;
self.bottomRect.height = bottomPipe.height;
return [self.topRect, self.bottomRect];
};
// Reset pipe position and gap
self.reset = function (x, gapY) {
self.x = x;
self.setGap(gapY);
self.gapY = gapY;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x6ec6ff // blue sky
});
/****
* Game Code
****/
// --- Game Variables ---
// --- Asset Initialization ---
var bird;
var pipes = [];
var pipeSpacing = 1500; // px between pipes (increased even more)
var pipeCount = 3;
var pipeMinY = 420;
var pipeMaxY = 2732 - 220 - 420;
var ground;
var score = 0;
var bestScore = storage.bestScore || 0;
var scoreTxt;
var bestScoreTxt;
var gameState = 'start'; // 'start', 'play', 'gameover'
var tapToStartTxt;
var dragNode = null;
// --- GUI Elements ---
scoreTxt = new Text2('0', {
size: 180,
fill: 0xFFF700,
font: "Impact, 'Arial Black', Tahoma"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
bestScoreTxt = new Text2('', {
size: 90,
fill: 0xFFFFFF,
font: "Impact, 'Arial Black', Tahoma"
});
bestScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(bestScoreTxt);
tapToStartTxt = new Text2('TAP TO START', {
size: 120,
fill: 0xFFFFFF,
font: "Impact, 'Arial Black', Tahoma"
});
tapToStartTxt.anchor.set(0.5, 0.5);
LK.gui.center.addChild(tapToStartTxt);
// --- Background and Ground ---
// Add clouds (simple white ellipses, different sizes, parallax)
var cloudAssets = [];
var cloudCount = 5;
for (var i = 0; i < cloudCount; i++) {
var cloud = LK.getAsset('ellipse', {
// use a white ellipse for cloud, not the bird image
anchorX: 0.5,
anchorY: 0.5,
width: 320 + Math.random() * 220,
height: 120 + Math.random() * 80,
x: Math.random() * 2048,
y: 200 + Math.random() * 600,
color: 0xffffff
});
cloud.alpha = 0.45 + Math.random() * 0.25;
game.addChild(cloud);
cloudAssets.push(cloud);
}
ground = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 2732 - 220
});
game.addChild(ground);
// Animate clouds in game.update
var _origUpdate = game.update;
game.update = function () {
// Move clouds slowly for parallax (always, even before first tap)
for (var i = 0; i < cloudAssets.length; i++) {
var c = cloudAssets[i];
c.x -= 0.7 + i * 0.15;
if (c.x < -300) {
c.x = 2048 + 200 * Math.random();
c.y = 200 + Math.random() * 600;
}
}
if (_origUpdate) _origUpdate.apply(this, arguments);
};
// --- Bird ---
bird = new Bird();
game.addChild(bird);
// --- Pipes ---
for (var i = 0; i < pipeCount; i++) {
var pipe = new PipePair();
pipes.push(pipe);
game.addChild(pipe);
}
// --- Helper Functions ---
function resetGame() {
// Reset state
score = 0;
scoreTxt.setText('0');
gameState = 'start';
tapToStartTxt.visible = true;
bestScoreTxt.visible = true;
bestScoreTxt.setText('BEST: ' + bestScore);
// Center bird
bird.reset(600, 1200);
bird._moveForward = false; // Prevent forward movement before first tap
// Reset pipes
var prevGapY = undefined;
for (var i = 0; i < pipes.length; i++) {
// First pipe is much further away from the bird
var px = i === 0 ? 2048 + 900 : 2048 + 900 + i * pipeSpacing * 1.25; // increase spacing even more
var gapY;
if (i === 0) {
// Always start first pipe gap near center
var centerY = Math.floor((pipeMinY + pipeMaxY) / 2);
gapY = centerY + Math.floor((Math.random() - 0.5) * 120); // within ±60px of center
} else {
// For subsequent pipes, gapY should not be exactly the same as previous
var maxGapDelta = 220; // keep initial difference reasonable
var minY = Math.max(pipeMinY, prevGapY - maxGapDelta);
var maxY = Math.min(pipeMaxY, prevGapY + maxGapDelta);
do {
gapY = minY + Math.floor(Math.random() * (maxY - minY));
} while (gapY === prevGapY && maxY > minY); // ensure not exactly the same as previous
}
pipes[i].reset(px, gapY);
pipes[i].gapY = gapY;
pipes[i].passed = false;
prevGapY = gapY;
}
}
function startGame() {
gameState = 'play';
tapToStartTxt.visible = false;
bestScoreTxt.visible = false;
score = 0;
scoreTxt.setText('0');
// Reset bird velocity
bird.vy = 0;
}
function gameOver() {
gameState = 'gameover';
// Flash screen red
LK.effects.flashScreen(0xff0000, 800);
// Update best score
if (score > bestScore) {
bestScore = score;
storage.bestScore = bestScore;
}
// Show game over popup (handled by LK)
LK.showGameOver();
}
// --- Input Handling ---
// Tap to jump or start
game.down = function (x, y, obj) {
if (gameState === 'start') {
startGame();
bird.vy = bird.jumpStrength;
bird.flap();
bird._moveForward = true;
} else if (gameState === 'play') {
bird.vy = bird.jumpStrength;
bird.flap();
bird._moveForward = true;
}
// No input in gameover (wait for LK reset)
};
// --- Main Update Loop ---
game.update = function () {
if (gameState === 'start') {
// Bird idle bobbing
bird.y = 1200 + Math.sin(LK.ticks / 20) * 30;
bird.vy = 0;
// Pipes do not move before first jump
// Prevent pipes from moving before first jump (on start screen)
// But allow clouds to move (background progress)
return;
}
if (gameState !== 'play') return;
// Bird physics
bird.update();
// Pipes
for (var i = 0; i < pipes.length; i++) {
var pipe = pipes[i];
pipe.update();
// Recycle pipe if off screen
if (pipe.x < -pipe.pipeWidth) {
var maxX = 0;
for (var j = 0; j < pipes.length; j++) {
if (pipes[j].x > maxX) maxX = pipes[j].x;
}
var px = maxX + pipeSpacing * 1.25; // increase spacing even more
// --- Gap height logic: keep new gapY close to previous, but never exactly the same as previous ---
var prevIndex = (i - 1 + pipes.length) % pipes.length;
var prevGapY = pipes[prevIndex].gapY !== undefined ? pipes[prevIndex].gapY : pipeMinY + Math.floor(Math.random() * (pipeMaxY - pipeMinY));
var maxGapDelta = 220 + Math.floor(score * 2); // Start with 220px, increase slowly with score
if (maxGapDelta > 600) maxGapDelta = 600; // Cap the max difference
var minY = Math.max(pipeMinY, prevGapY - maxGapDelta);
var maxY = Math.min(pipeMaxY, prevGapY + maxGapDelta);
var gapY;
do {
gapY = minY + Math.floor(Math.random() * (maxY - minY));
} while (gapY === prevGapY && maxY > minY); // ensure not exactly the same as previous
// Store gapY for next pipe logic
pipe.gapY = gapY;
pipe.reset(px, gapY);
pipe.passed = false;
}
// Score: if bird passes pipe
if (!pipe.passed && pipe.x + pipe.pipeWidth < bird.x - bird.radius) {
pipe.passed = true;
score += 1;
scoreTxt.setText(score + '');
}
}
// Collision detection
// 1. Ground
if (bird.y + bird.radius > 2732 - 220) {
bird.y = 2732 - 220 - bird.radius;
gameOver();
return;
}
// 2. Ceiling
if (bird.y - bird.radius < 0) {
bird.y = bird.radius;
gameOver();
return;
}
// 3. Pipes
for (var i = 0; i < pipes.length; i++) {
var rects = pipes[i].getRects();
for (var r = 0; r < rects.length; r++) {
if (circleRectCollide(bird.x, bird.y, bird.radius, rects[r])) {
gameOver();
return;
}
}
}
};
// --- Utility: Circle-Rectangle Collision ---
function circleRectCollide(cx, cy, cr, rect) {
// Find closest point to circle within rectangle
var closestX = Math.max(rect.x, Math.min(cx, rect.x + rect.width));
var closestY = Math.max(rect.y, Math.min(cy, rect.y + rect.height));
var dx = cx - closestX;
var dy = cy - closestY;
return dx * dx + dy * dy < cr * cr;
}
// --- Game Over Handler (reset on LK reset) ---
LK.on('gameover', function () {
resetGame();
});
// --- Initial State ---
resetGame(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
bestScore: 0
});
/****
* Classes
****/
// Bird class
var Bird = Container.expand(function () {
var self = Container.call(this);
// Attach yellow ellipse as bird (now a shape, not an image)
var birdAsset = self.attachAsset('bird', {
anchorX: 0.5,
anchorY: 0.5
});
// Physics
self.vy = 0;
self.gravity = 0.45; // px per frame^2 (even slower fall)
self.jumpStrength = -14; // px per frame (even lower jump, less height per tap)
// Bird size for collision
self.radius = birdAsset.width * 0.45;
// Flap animation (squash/stretch)
self.flap = function () {
// Animate scale for a quick squash
tween.stop(birdAsset, {
scaleY: true,
scaleX: true
});
birdAsset.scaleY = 1.0;
birdAsset.scaleX = 1.0;
tween(birdAsset, {
scaleY: 0.7,
scaleX: 1.2
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
tween(birdAsset, {
scaleY: 1.0,
scaleX: 1.0
}, {
duration: 120,
easing: tween.cubicIn
});
}
});
};
// Bird physics update
self.update = function () {
self.vy += self.gravity;
self.y += self.vy;
// Only move forward (x) if the player has tapped (i.e. on jump)
if (gameState === 'play' && self._moveForward) {
self.x += 3; // Move forward even more slowly per jump
self._moveForward = false;
}
// Clamp rotation based on vy
var maxAngle = Math.PI / 4;
var minAngle = -Math.PI / 6;
var t = Math.max(-20, Math.min(20, self.vy)) / 20;
birdAsset.rotation = minAngle + (maxAngle - minAngle) * ((t + 1) / 2);
};
// Reset bird state
self.reset = function (x, y) {
self.x = x;
self.y = y;
self.vy = 0;
birdAsset.rotation = 0;
birdAsset.scaleX = 1.0;
birdAsset.scaleY = 1.0;
};
return self;
});
// PipePair class (top and bottom pipes)
var PipePair = Container.expand(function () {
var self = Container.call(this);
// Pipe config
self.pipeWidth = 220;
self.gapHeight = 520;
self.speed = 12; // px per frame
// Top pipe
var topPipe = self.attachAsset('pipe', {
anchorX: 0,
anchorY: 1,
width: self.pipeWidth,
height: 900,
y: 0
});
// Bottom pipe
var bottomPipe = self.attachAsset('pipe', {
anchorX: 0,
anchorY: 0,
width: self.pipeWidth,
height: 900,
y: 0
});
// For collision
self.topRect = new Rectangle();
self.bottomRect = new Rectangle();
// Set vertical gap position
self.setGap = function (gapY) {
// gapY is the center of the gap
var pipeTop = gapY - self.gapHeight / 2;
var pipeBottom = gapY + self.gapHeight / 2;
topPipe.height = Math.max(80, pipeTop);
topPipe.y = pipeTop;
bottomPipe.height = Math.max(80, 2732 - pipeBottom - 220);
bottomPipe.y = pipeBottom;
};
// Update position
self.update = function () {
self.x -= self.speed;
};
// Get rectangles for collision
self.getRects = function () {
// Top pipe
self.topRect.x = self.x;
self.topRect.y = 0;
self.topRect.width = self.pipeWidth;
self.topRect.height = topPipe.height;
// Bottom pipe
self.bottomRect.x = self.x;
self.bottomRect.y = bottomPipe.y;
self.bottomRect.width = self.pipeWidth;
self.bottomRect.height = bottomPipe.height;
return [self.topRect, self.bottomRect];
};
// Reset pipe position and gap
self.reset = function (x, gapY) {
self.x = x;
self.setGap(gapY);
self.gapY = gapY;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x6ec6ff // blue sky
});
/****
* Game Code
****/
// --- Game Variables ---
// --- Asset Initialization ---
var bird;
var pipes = [];
var pipeSpacing = 1500; // px between pipes (increased even more)
var pipeCount = 3;
var pipeMinY = 420;
var pipeMaxY = 2732 - 220 - 420;
var ground;
var score = 0;
var bestScore = storage.bestScore || 0;
var scoreTxt;
var bestScoreTxt;
var gameState = 'start'; // 'start', 'play', 'gameover'
var tapToStartTxt;
var dragNode = null;
// --- GUI Elements ---
scoreTxt = new Text2('0', {
size: 180,
fill: 0xFFF700,
font: "Impact, 'Arial Black', Tahoma"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
bestScoreTxt = new Text2('', {
size: 90,
fill: 0xFFFFFF,
font: "Impact, 'Arial Black', Tahoma"
});
bestScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(bestScoreTxt);
tapToStartTxt = new Text2('TAP TO START', {
size: 120,
fill: 0xFFFFFF,
font: "Impact, 'Arial Black', Tahoma"
});
tapToStartTxt.anchor.set(0.5, 0.5);
LK.gui.center.addChild(tapToStartTxt);
// --- Background and Ground ---
// Add clouds (simple white ellipses, different sizes, parallax)
var cloudAssets = [];
var cloudCount = 5;
for (var i = 0; i < cloudCount; i++) {
var cloud = LK.getAsset('ellipse', {
// use a white ellipse for cloud, not the bird image
anchorX: 0.5,
anchorY: 0.5,
width: 320 + Math.random() * 220,
height: 120 + Math.random() * 80,
x: Math.random() * 2048,
y: 200 + Math.random() * 600,
color: 0xffffff
});
cloud.alpha = 0.45 + Math.random() * 0.25;
game.addChild(cloud);
cloudAssets.push(cloud);
}
ground = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 2732 - 220
});
game.addChild(ground);
// Animate clouds in game.update
var _origUpdate = game.update;
game.update = function () {
// Move clouds slowly for parallax (always, even before first tap)
for (var i = 0; i < cloudAssets.length; i++) {
var c = cloudAssets[i];
c.x -= 0.7 + i * 0.15;
if (c.x < -300) {
c.x = 2048 + 200 * Math.random();
c.y = 200 + Math.random() * 600;
}
}
if (_origUpdate) _origUpdate.apply(this, arguments);
};
// --- Bird ---
bird = new Bird();
game.addChild(bird);
// --- Pipes ---
for (var i = 0; i < pipeCount; i++) {
var pipe = new PipePair();
pipes.push(pipe);
game.addChild(pipe);
}
// --- Helper Functions ---
function resetGame() {
// Reset state
score = 0;
scoreTxt.setText('0');
gameState = 'start';
tapToStartTxt.visible = true;
bestScoreTxt.visible = true;
bestScoreTxt.setText('BEST: ' + bestScore);
// Center bird
bird.reset(600, 1200);
bird._moveForward = false; // Prevent forward movement before first tap
// Reset pipes
var prevGapY = undefined;
for (var i = 0; i < pipes.length; i++) {
// First pipe is much further away from the bird
var px = i === 0 ? 2048 + 900 : 2048 + 900 + i * pipeSpacing * 1.25; // increase spacing even more
var gapY;
if (i === 0) {
// Always start first pipe gap near center
var centerY = Math.floor((pipeMinY + pipeMaxY) / 2);
gapY = centerY + Math.floor((Math.random() - 0.5) * 120); // within ±60px of center
} else {
// For subsequent pipes, gapY should not be exactly the same as previous
var maxGapDelta = 220; // keep initial difference reasonable
var minY = Math.max(pipeMinY, prevGapY - maxGapDelta);
var maxY = Math.min(pipeMaxY, prevGapY + maxGapDelta);
do {
gapY = minY + Math.floor(Math.random() * (maxY - minY));
} while (gapY === prevGapY && maxY > minY); // ensure not exactly the same as previous
}
pipes[i].reset(px, gapY);
pipes[i].gapY = gapY;
pipes[i].passed = false;
prevGapY = gapY;
}
}
function startGame() {
gameState = 'play';
tapToStartTxt.visible = false;
bestScoreTxt.visible = false;
score = 0;
scoreTxt.setText('0');
// Reset bird velocity
bird.vy = 0;
}
function gameOver() {
gameState = 'gameover';
// Flash screen red
LK.effects.flashScreen(0xff0000, 800);
// Update best score
if (score > bestScore) {
bestScore = score;
storage.bestScore = bestScore;
}
// Show game over popup (handled by LK)
LK.showGameOver();
}
// --- Input Handling ---
// Tap to jump or start
game.down = function (x, y, obj) {
if (gameState === 'start') {
startGame();
bird.vy = bird.jumpStrength;
bird.flap();
bird._moveForward = true;
} else if (gameState === 'play') {
bird.vy = bird.jumpStrength;
bird.flap();
bird._moveForward = true;
}
// No input in gameover (wait for LK reset)
};
// --- Main Update Loop ---
game.update = function () {
if (gameState === 'start') {
// Bird idle bobbing
bird.y = 1200 + Math.sin(LK.ticks / 20) * 30;
bird.vy = 0;
// Pipes do not move before first jump
// Prevent pipes from moving before first jump (on start screen)
// But allow clouds to move (background progress)
return;
}
if (gameState !== 'play') return;
// Bird physics
bird.update();
// Pipes
for (var i = 0; i < pipes.length; i++) {
var pipe = pipes[i];
pipe.update();
// Recycle pipe if off screen
if (pipe.x < -pipe.pipeWidth) {
var maxX = 0;
for (var j = 0; j < pipes.length; j++) {
if (pipes[j].x > maxX) maxX = pipes[j].x;
}
var px = maxX + pipeSpacing * 1.25; // increase spacing even more
// --- Gap height logic: keep new gapY close to previous, but never exactly the same as previous ---
var prevIndex = (i - 1 + pipes.length) % pipes.length;
var prevGapY = pipes[prevIndex].gapY !== undefined ? pipes[prevIndex].gapY : pipeMinY + Math.floor(Math.random() * (pipeMaxY - pipeMinY));
var maxGapDelta = 220 + Math.floor(score * 2); // Start with 220px, increase slowly with score
if (maxGapDelta > 600) maxGapDelta = 600; // Cap the max difference
var minY = Math.max(pipeMinY, prevGapY - maxGapDelta);
var maxY = Math.min(pipeMaxY, prevGapY + maxGapDelta);
var gapY;
do {
gapY = minY + Math.floor(Math.random() * (maxY - minY));
} while (gapY === prevGapY && maxY > minY); // ensure not exactly the same as previous
// Store gapY for next pipe logic
pipe.gapY = gapY;
pipe.reset(px, gapY);
pipe.passed = false;
}
// Score: if bird passes pipe
if (!pipe.passed && pipe.x + pipe.pipeWidth < bird.x - bird.radius) {
pipe.passed = true;
score += 1;
scoreTxt.setText(score + '');
}
}
// Collision detection
// 1. Ground
if (bird.y + bird.radius > 2732 - 220) {
bird.y = 2732 - 220 - bird.radius;
gameOver();
return;
}
// 2. Ceiling
if (bird.y - bird.radius < 0) {
bird.y = bird.radius;
gameOver();
return;
}
// 3. Pipes
for (var i = 0; i < pipes.length; i++) {
var rects = pipes[i].getRects();
for (var r = 0; r < rects.length; r++) {
if (circleRectCollide(bird.x, bird.y, bird.radius, rects[r])) {
gameOver();
return;
}
}
}
};
// --- Utility: Circle-Rectangle Collision ---
function circleRectCollide(cx, cy, cr, rect) {
// Find closest point to circle within rectangle
var closestX = Math.max(rect.x, Math.min(cx, rect.x + rect.width));
var closestY = Math.max(rect.y, Math.min(cy, rect.y + rect.height));
var dx = cx - closestX;
var dy = cy - closestY;
return dx * dx + dy * dy < cr * cr;
}
// --- Game Over Handler (reset on LK reset) ---
LK.on('gameover', function () {
resetGame();
});
// --- Initial State ---
resetGame();
A yellow bird. In pixel art style. The bird should be just like the head part.. In-Game asset. 2d. High contrast. No shadows
white cloud. In-Game asset. 2d. High contrast. No shadows
the soil covering the entire image and extending horizontally and the greenery (short grass) on top of it. In-Game asset. 2d. High contrast. No shadows
green pipe. In-Game asset. 2d. High contrast. No shadows