/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Bird class
var Bird = Container.expand(function () {
var self = Container.call(this);
var birdSprite = self.attachAsset('bird', {
anchorX: 0.5,
anchorY: 0.5
});
var birdDrinkSprite = self.attachAsset('Bird_drink', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
});
var drink2Sprite = self.attachAsset('Drink_2', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
});
var drink3Sprite = self.attachAsset('Drink3', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
});
// Position drink in front of bird
birdDrinkSprite.x = birdSprite.width * 0.7; // Offset to the right (in front)
drink2Sprite.x = birdDrinkSprite.x; // Align drink2Sprite with birdDrinkSprite
drink3Sprite.x = birdDrinkSprite.x; // Align drink3Sprite with birdDrinkSprite
self.currentSprite = birdSprite;
self.radius = birdSprite.width * 0.5 * 0.85; // for collision
self.vy = 0; // vertical speed
self.gravity = 0.6; // gravity per frame (slower fall)
self.flapStrength = -22; // negative = up, less strong flap
// Flap method
self.flap = function () {
self.vy = self.flapStrength;
LK.getSound('flap').play();
};
// Toggle drink method
self.toggleDrink = function () {
// Cycle through no drink, bird drink, drink 2, and drink 3
if (!birdDrinkSprite.visible && !drink2Sprite.visible && !drink3Sprite.visible) {
birdDrinkSprite.visible = true;
drink2Sprite.visible = false;
drink3Sprite.visible = false;
} else if (birdDrinkSprite.visible) {
birdDrinkSprite.visible = false;
drink2Sprite.visible = true;
drink3Sprite.visible = false;
} else if (drink2Sprite.visible) {
birdDrinkSprite.visible = false;
drink2Sprite.visible = false;
drink3Sprite.visible = true;
} else {
birdDrinkSprite.visible = false;
drink2Sprite.visible = false;
drink3Sprite.visible = false;
}
// Save the current drink state to storage
storage.lastDrink = birdDrinkSprite.visible ? 1 : drink2Sprite.visible ? 2 : 0;
};
// Update method
self.update = function () {
self.vy += self.gravity * (slowTimeActive ? slowTimeMultiplier : 1);
self.y += self.vy;
// Rotate bird based on velocity (visual only)
var maxAngle = Math.PI / 4;
var minAngle = -Math.PI / 6;
var angle = self.vy / 40 * maxAngle;
if (angle > maxAngle) angle = maxAngle;
if (angle < minAngle) angle = minAngle;
birdSprite.rotation = angle;
// Keep drink upright and in front
birdDrinkSprite.rotation = 0;
};
return self;
});
// Fire class for standalone fire effects
var Fire = Container.expand(function () {
var self = Container.call(this);
var fireSprite = self.attachAsset('fire', {
anchorX: 0.5,
anchorY: 0.5,
visible: false,
alpha: 0.5
});
self.shootFire = function () {
fireSprite.visible = true;
fireSprite.x = 0; // Reset position when shooting
fireSprite.y = 0;
tween(fireSprite, {
alpha: 1
}, {
duration: 500,
easing: tween.linear,
onFinish: function onFinish() {
tween(fireSprite, {
alpha: 0.5
}, {
duration: 500,
easing: tween.linear
});
}
});
};
self.getBoundingRect = function () {
if (!fireSprite.visible) return null;
return {
x: self.x + fireSprite.x - fireSprite.width * 0.5,
y: self.y + fireSprite.y - fireSprite.height * 0.5,
width: fireSprite.width,
height: fireSprite.height
};
};
self.update = function () {
if (fireSprite.visible) {
fireSprite.x -= pipeSpeed;
if (fireSprite.x < -fireSprite.width) {
fireSprite.visible = false;
}
}
};
return self;
});
var FirePipe = Container.expand(function () {
var self = Container.call(this);
var firePipeSprite = self.attachAsset('FirePipe', {
anchorX: 0.5,
anchorY: 0.5,
visible: true,
alpha: 1
});
self.movingRight = true; // Track movement direction
self.startMovement = function () {
// Start continuous left-right movement
self.moveLeftRight();
};
self.moveLeftRight = function () {
// Check if this pipe is frozen
if (self.frozen) return;
var targetX = self.movingRight ? 300 : -300; // Move 300px in each direction
tween(firePipeSprite, {
x: targetX
}, {
duration: 2000,
// 2 seconds to move in one direction
easing: tween.easeInOut,
onFinish: function onFinish() {
// Reverse direction and continue moving
self.movingRight = !self.movingRight;
self.moveLeftRight();
}
});
};
self.freeze = function () {
self.frozen = true;
// Add visual effect for frozen state
tween(firePipeSprite, {
tint: 0x88ccff
}, {
duration: 300,
easing: tween.easeOut
});
};
self.unfreeze = function () {
self.frozen = false;
// Remove visual effect
tween(firePipeSprite, {
tint: 0xffffff
}, {
duration: 300,
easing: tween.easeOut
});
// Resume movement
self.moveLeftRight();
};
self.getBoundingRect = function () {
if (!firePipeSprite.visible) return null;
return {
x: self.x + firePipeSprite.x - firePipeSprite.width * 0.5,
y: self.y + firePipeSprite.y - firePipeSprite.height * 0.5,
width: firePipeSprite.width,
height: firePipeSprite.height
};
};
self.update = function () {
// Movement is now handled by tween
};
return self;
});
// PipePair class (top and bottom pipes)
var PipePair = Container.expand(function () {
var self = Container.call(this);
// Pipe config
var pipeWidth = 200;
var pipeHeight = 1200;
var gapHeight = 480; // vertical gap between pipes
// Randomly decide if pipes are flipped (top/bottom swap orientation)
var flip = Math.random() < 0.5;
// Randomly decide pipe type: normal (70%) or fire (30%)
var isFirePipe = Math.random() < 0.3;
// For fire pipes, always single pipe. For regular pipes, 30% chance of single pipe
var isSinglePipe = isFirePipe || Math.random() < 0.3;
var pipeAsset = isFirePipe ? 'FirePipe' : 'pipe';
var topPipe = null;
var bottomPipe = null;
// Create pipes based on configuration
if (isSinglePipe) {
// Single pipe - randomly choose top or bottom position
var isTopPosition = Math.random() < 0.5;
if (isTopPosition) {
topPipe = self.attachAsset(pipeAsset, {
anchorX: 0,
anchorY: flip ? 0 : 1,
width: pipeWidth,
height: pipeHeight,
rotation: flip ? Math.PI : 0
});
} else {
bottomPipe = self.attachAsset(pipeAsset, {
anchorX: 0,
anchorY: flip ? 1 : 0,
width: pipeWidth,
height: pipeHeight,
rotation: flip ? Math.PI : 0
});
}
} else {
// Double pipes (traditional setup)
topPipe = self.attachAsset(pipeAsset, {
anchorX: 0,
anchorY: flip ? 0 : 1,
width: pipeWidth,
height: pipeHeight,
rotation: flip ? Math.PI : 0
});
bottomPipe = self.attachAsset(pipeAsset, {
anchorX: 0,
anchorY: flip ? 1 : 0,
width: pipeWidth,
height: pipeHeight,
rotation: flip ? Math.PI : 0
});
}
self.pipeWidth = pipeWidth;
self.gapHeight = gapHeight;
self.passed = false; // for scoring
self.isFirePipe = isFirePipe;
self.isSinglePipe = isSinglePipe;
self.fireTimer = 0;
self.topPipe = topPipe;
self.bottomPipe = bottomPipe;
// Set pipes' vertical positions based on gapY (top of gap)
self.setGap = function (gapY) {
// gapY: y position of top of gap
if (topPipe) topPipe.y = gapY;
if (bottomPipe) bottomPipe.y = gapY + gapHeight;
};
// Move pipes left
self.update = function () {
self.x -= pipeSpeed;
};
// Get bounding rects for collision
self.getTopRect = function () {
if (!topPipe) return null;
return {
x: self.x,
y: topPipe.y - pipeHeight,
width: pipeWidth,
height: pipeHeight
};
};
self.getBottomRect = function () {
if (!bottomPipe) return null;
return {
x: self.x,
y: bottomPipe.y,
width: pipeWidth,
height: pipeHeight
};
};
// Add fire shooting capability for fire pipes
if (isFirePipe) {
var firePipe = new FirePipe();
self.addChild(firePipe);
self.firePipe = firePipe;
self.fireStarted = false; // Track if fire has been started
}
self.update = function () {
self.x -= pipeSpeed * (slowTimeActive ? slowTimeMultiplier : 1);
// Update fire pipe if this is a fire pipe
if (self.isFirePipe && self.firePipe) {
self.firePipe.x = self.x; // Position fire at pipe position
// Center fire in the gap or at pipe level for single pipes
var fireY = bottomPipe ? bottomPipe.y - gapHeight / 2 : topPipe ? topPipe.y + gapHeight / 2 : 1366;
self.firePipe.y = fireY;
self.firePipe.update();
// Start movement when pipe enters screen (only once)
if (!self.fireStarted && self.x < 2048) {
self.fireStarted = true;
// Check if power-up is active and freeze immediately
if (slowTimeActive) {
self.firePipe.freeze();
frozenPipes.push(self.firePipe);
} else {
self.firePipe.startMovement();
}
}
}
};
return self;
});
// PowerUp class
var PowerUp = Container.expand(function () {
var self = Container.call(this);
// Create a star-shaped power-up visual using new asset
var powerUpSprite = self.attachAsset('powerUpStar', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x00ffff // Cyan color for freeze power-up
});
// Add pulsing effect
self.pulse = function () {
tween(powerUpSprite, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(powerUpSprite, {
scaleX: 1,
scaleY: 1
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
self.pulse();
}
});
}
});
};
self.collected = false;
self.update = function () {
if (!self.collected) {
self.x -= pipeSpeed * (slowTimeActive ? slowTimeMultiplier : 1);
}
};
self.getBoundingRect = function () {
return {
x: self.x - powerUpSprite.width * 0.5,
y: self.y - powerUpSprite.height * 0.5,
width: powerUpSprite.width,
height: powerUpSprite.height
};
};
// Start pulsing animation
self.pulse();
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87ceeb // sky blue
});
/****
* Game Code
****/
// Sound: Hit
// Sound: Score
// Sound: Flap
// Ground: brown box
// Pipe: green box
// Bird: yellow ellipse
// Game constants
var BIRD_START_X = 600;
var BIRD_START_Y = 1200;
var PIPE_INTERVAL = 1300; // px between pipes horizontally (slower spawn rate)
var PIPE_MIN_Y = 350; // min y for top of gap
var PIPE_MAX_Y = 1800; // max y for top of gap
var GROUND_Y = 2732 - 120; // ground top y
var pipeSpeed = 8; // px per frame (slower pipes)
var POWERUP_DURATION = 30000; // 30 seconds in milliseconds
var slowTimeMultiplier = 0.3; // 30% speed during slow time
// Game state
var bird;
var pipes = [];
var powerUps = [];
var ground;
var score = 0;
var scoreTxt;
var gameOver = false;
var started = false;
var lastPipeX = 0;
var slowTimeActive = false;
var slowTimeTimer = null;
var frozenPipes = [];
// GUI: Score
scoreTxt = new Text2('0', {
size: 160,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// GUI: Game Over text
var gameOverTxt = new Text2('Game Over', {
size: 180,
fill: 0xFF4444
});
gameOverTxt.anchor.set(0.5, 0.5);
gameOverTxt.visible = false;
LK.gui.center.addChild(gameOverTxt);
// GUI: Tap to restart
var restartTxt = new Text2('Tap to restart', {
size: 100,
fill: 0xFFFFFF
});
restartTxt.anchor.set(0.5, 0.5);
restartTxt.visible = false;
LK.gui.center.addChild(restartTxt);
// GUI: Drink toggle button
var drinkToggleBtn = new Container();
var drinkBtnBg = drinkToggleBtn.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 150,
tint: 0x4444ff
});
var drinkBtnText = new Text2('🥤', {
size: 80,
fill: 0xFFFFFF
});
drinkBtnText.anchor.set(0.5, 0.5);
drinkToggleBtn.addChild(drinkBtnText);
drinkToggleBtn.x = -100;
drinkToggleBtn.y = 100;
LK.gui.topRight.addChild(drinkToggleBtn);
// GUI: Reset score button
var resetScoreBtn = new Container();
var resetBtnBg = resetScoreBtn.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 150,
tint: 0xff4444
});
var resetBtnText = new Text2('Ø', {
size: 100,
fill: 0xFFFFFF
});
resetBtnText.anchor.set(0.5, 0.5);
resetScoreBtn.addChild(resetBtnText);
resetScoreBtn.x = -100;
resetScoreBtn.y = 280;
LK.gui.topRight.addChild(resetScoreBtn);
// GUI: Bird swapper button
var birdSwapBtn = new Container();
var birdSwapBtnBg = birdSwapBtn.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 150,
tint: 0x44ff44
});
var birdSwapIcon = birdSwapBtn.attachAsset('bird', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
birdSwapBtn.x = -100;
birdSwapBtn.y = 460;
LK.gui.topRight.addChild(birdSwapBtn);
// Start spinning animation for reset button
function spinResetButton() {
tween(resetBtnText, {
rotation: resetBtnText.rotation + Math.PI * 2
}, {
duration: 2000,
easing: tween.linear,
onFinish: function onFinish() {
spinResetButton();
}
});
}
spinResetButton();
// GUI: Power-up indicator
var powerUpIndicator = new Container();
var powerUpBg = powerUpIndicator.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 200,
height: 80,
tint: 0x00ffff
});
var powerUpText = new Text2('FREEZE: 30s', {
size: 40,
fill: 0xFFFFFF
});
powerUpText.anchor.set(0.5, 0.5);
powerUpIndicator.addChild(powerUpText);
powerUpIndicator.x = 0;
powerUpIndicator.y = 200;
powerUpIndicator.visible = false;
LK.gui.top.addChild(powerUpIndicator);
// Drink toggle handler
drinkToggleBtn.down = function () {
if (bird) {
bird.toggleDrink();
}
};
// Reset score handler
resetScoreBtn.down = function () {
score = 0;
scoreTxt.setText('0');
storage.lastScore = 0;
// Flash effect to indicate reset
LK.effects.flashObject(resetScoreBtn, 0xffffff, 300);
};
// Bird swap handler
birdSwapBtn.down = function () {
if (bird) {
// Get current bird type from storage or default to 'bird'
var currentBirdType = storage.currentBirdType || 'bird';
var newBirdType = currentBirdType === 'bird' ? 'Bird2' : 'bird';
// Store current drink state
var drinkState = {
drink1: bird.children[1].visible,
drink2: bird.children[2].visible,
drink3: bird.children[3].visible
};
// Remove current main bird sprite
var currentBird = bird.children[0];
bird.removeChild(currentBird);
// Create new bird sprite
var newBird = LK.getAsset(newBirdType, {
anchorX: 0.5,
anchorY: 0.5
});
bird.addChildAt(newBird, 0);
bird.currentSprite = newBird;
// Update rotation to match current velocity
var maxAngle = Math.PI / 4;
var minAngle = -Math.PI / 6;
var angle = bird.vy / 40 * maxAngle;
if (angle > maxAngle) angle = maxAngle;
if (angle < minAngle) angle = minAngle;
newBird.rotation = angle;
// Restore drink state
bird.children[1].visible = drinkState.drink1;
bird.children[2].visible = drinkState.drink2;
bird.children[3].visible = drinkState.drink3;
// Save new bird type
storage.currentBirdType = newBirdType;
// Flash effect to indicate swap
LK.effects.flashObject(birdSwapBtn, 0xffffff, 300);
}
};
// Add ground
ground = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0,
x: 0,
y: GROUND_Y
});
game.addChild(ground);
// Helper: Reset game state
function resetGame() {
// Remove pipes
for (var i = 0; i < pipes.length; i++) {
pipes[i].destroy();
}
pipes = [];
// Remove power-ups
for (var i = 0; i < powerUps.length; i++) {
powerUps[i].destroy();
}
powerUps = [];
// Clear power-up state
if (slowTimeTimer) {
LK.clearTimeout(slowTimeTimer);
slowTimeTimer = null;
}
slowTimeActive = false;
powerUpIndicator.visible = false;
frozenPipes = [];
// Remove bird if exists
if (bird) bird.destroy();
// Reset variables
score = 0;
scoreTxt.setText('0');
gameOver = false;
started = false;
lastPipeX = 0;
// Hide game over
gameOverTxt.visible = false;
restartTxt.visible = false;
// Add bird
bird = new Bird();
bird.x = BIRD_START_X;
bird.y = BIRD_START_Y;
game.addChild(bird);
// Restore score if available
if (typeof storage.lastScore !== "undefined") {
score = storage.lastScore;
scoreTxt.setText(score + '');
}
// Restore bird type if available
if (typeof storage.currentBirdType !== "undefined" && storage.currentBirdType === 'Bird2') {
var currentBird = bird.children[0];
var newBird = LK.getAsset('Bird2', {
anchorX: 0.5,
anchorY: 0.5
});
bird.removeChild(currentBird);
bird.addChildAt(newBird, 0);
bird.currentSprite = newBird;
}
// Restore drink state if available
if (typeof storage.lastDrink !== "undefined") {
if (storage.lastDrink === 1) {
bird.children[1].visible = true;
bird.children[2].visible = false;
} else if (storage.lastDrink === 2) {
bird.children[1].visible = false;
bird.children[2].visible = true;
} else if (storage.lastDrink === 3) {
bird.children[1].visible = false;
bird.children[2].visible = false;
bird.children[3].visible = true;
} else {
bird.children[1].visible = false;
bird.children[2].visible = false;
bird.children[3].visible = false;
}
}
// Add first pipes
for (var i = 0; i < 3; i++) {
spawnPipe(2048 + i * PIPE_INTERVAL);
}
}
// Helper: Spawn a pipe pair at x
function spawnPipe(x) {
var gapY = PIPE_MIN_Y + Math.floor(Math.random() * (PIPE_MAX_Y - PIPE_MIN_Y));
var pipePair = new PipePair();
pipePair.x = x;
pipePair.setGap(gapY);
pipes.push(pipePair);
game.addChild(pipePair);
lastPipeX = x;
// 10% chance to spawn power-up with pipe
if (Math.random() < 0.1 && !slowTimeActive) {
var powerUp = new PowerUp();
powerUp.x = x + pipePair.pipeWidth / 2;
powerUp.y = gapY + pipePair.gapHeight / 2;
powerUps.push(powerUp);
game.addChild(powerUp);
}
}
// Helper: Check collision between bird and a pipe rect
function birdHitsRect(rect) {
// Return false if rect is null (e.g., when fire sprite is not visible)
if (!rect) return false;
// Bird is a circle, rect is {x, y, width, height}
var cx = bird.x;
var cy = bird.y;
var r = bird.radius;
// Find closest point in rect to bird center
var closestX = cx;
if (cx < rect.x) closestX = rect.x;else if (cx > rect.x + rect.width) closestX = rect.x + rect.width;
var closestY = cy;
if (cy < rect.y) closestY = rect.y;else if (cy > rect.y + rect.height) closestY = rect.y + rect.height;
var dx = cx - closestX;
var dy = cy - closestY;
return dx * dx + dy * dy < r * r;
}
// Helper: Activate power-up
function activatePowerUp() {
slowTimeActive = true;
powerUpIndicator.visible = true;
// Freeze all fire pipes
for (var i = 0; i < pipes.length; i++) {
if (pipes[i].isFirePipe && pipes[i].firePipe) {
pipes[i].firePipe.freeze();
frozenPipes.push(pipes[i].firePipe);
}
}
// Start countdown
var timeLeft = POWERUP_DURATION / 1000;
var countdownInterval = LK.setInterval(function () {
timeLeft--;
powerUpText.setText('FREEZE: ' + timeLeft + 's');
if (timeLeft <= 0) {
LK.clearInterval(countdownInterval);
}
}, 1000);
// Set timer to deactivate power-up
if (slowTimeTimer) {
LK.clearTimeout(slowTimeTimer);
}
slowTimeTimer = LK.setTimeout(function () {
slowTimeActive = false;
powerUpIndicator.visible = false;
// Unfreeze all frozen pipes
for (var i = 0; i < frozenPipes.length; i++) {
frozenPipes[i].unfreeze();
}
frozenPipes = [];
LK.clearInterval(countdownInterval);
}, POWERUP_DURATION);
}
// Helper: End game
function triggerGameOver() {
if (gameOver) return;
gameOver = true;
// Save score and drink state to storage
storage.lastScore = score;
// Access birdDrinkSprite via bird instance if available
storage.lastDrink = bird && bird.hasOwnProperty('children') && bird.children.length > 1 ? bird.children[1].visible ? 1 : bird.children[2].visible ? 2 : bird.children[3].visible ? 3 : 0 : 0;
LK.getSound('hit').play();
gameOverTxt.visible = true;
restartTxt.visible = true;
// Flash screen
LK.effects.flashScreen(0xff0000, 600);
// Show Game Over popup (will pause game)
// LK.showGameOver();
}
// Game tap/flap handler
function handleTap(x, y, obj) {
if (gameOver) {
resetGame();
return;
}
if (!started) {
started = true;
}
bird.flap();
}
// Attach tap handler
game.down = handleTap;
// Main update loop
game.update = function () {
if (gameOver) return;
// Bird update
bird.update();
// Pipes update
for (var i = pipes.length - 1; i >= 0; i--) {
var pipe = pipes[i];
pipe.update();
// Remove pipes off screen
if (pipe.x + pipe.pipeWidth < 0) {
pipe.destroy();
pipes.splice(i, 1);
continue;
}
// Scoring: passed pipe
if (!pipe.passed && pipe.x + pipe.pipeWidth < bird.x) {
pipe.passed = true;
score += 1;
scoreTxt.setText(score + '');
LK.getSound('score').play();
}
}
// Spawn new pipes
if (pipes.length > 0) {
var rightmost = pipes[pipes.length - 1];
if (2048 - (rightmost.x + rightmost.pipeWidth) >= PIPE_INTERVAL - 10) {
spawnPipe(2048);
}
}
// Power-ups update and collision
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
powerUp.update();
// Remove power-ups off screen
if (powerUp.x + 40 < 0) {
powerUp.destroy();
powerUps.splice(i, 1);
continue;
}
// Check collision with bird
if (!powerUp.collected && birdHitsRect(powerUp.getBoundingRect())) {
powerUp.collected = true;
powerUp.visible = false;
activatePowerUp();
LK.getSound('score').play();
}
}
// Collision: pipes
for (var i = 0; i < pipes.length; i++) {
var pipe = pipes[i];
var hitTopPipe = birdHitsRect(pipe.getTopRect());
var hitBottomPipe = birdHitsRect(pipe.getBottomRect());
var hitPipe = hitTopPipe || hitBottomPipe;
var hitFire = pipe.isFirePipe && pipe.firePipe && pipe.firePipe.getBoundingRect && birdHitsRect(pipe.firePipe.getBoundingRect());
if (hitPipe || hitFire) {
triggerGameOver();
return;
}
}
// Collision: ground
if (bird.y + bird.radius > GROUND_Y) {
bird.y = GROUND_Y - bird.radius;
triggerGameOver();
return;
}
// Collision: ceiling
if (bird.y - bird.radius < 0) {
bird.y = bird.radius;
bird.vy = 0;
}
// Out of bounds (left/right)
if (bird.x - bird.radius < 0 || bird.x + bird.radius > 2048) {
triggerGameOver();
return;
}
};
// Initial game state
resetGame(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Bird class
var Bird = Container.expand(function () {
var self = Container.call(this);
var birdSprite = self.attachAsset('bird', {
anchorX: 0.5,
anchorY: 0.5
});
var birdDrinkSprite = self.attachAsset('Bird_drink', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
});
var drink2Sprite = self.attachAsset('Drink_2', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
});
var drink3Sprite = self.attachAsset('Drink3', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
});
// Position drink in front of bird
birdDrinkSprite.x = birdSprite.width * 0.7; // Offset to the right (in front)
drink2Sprite.x = birdDrinkSprite.x; // Align drink2Sprite with birdDrinkSprite
drink3Sprite.x = birdDrinkSprite.x; // Align drink3Sprite with birdDrinkSprite
self.currentSprite = birdSprite;
self.radius = birdSprite.width * 0.5 * 0.85; // for collision
self.vy = 0; // vertical speed
self.gravity = 0.6; // gravity per frame (slower fall)
self.flapStrength = -22; // negative = up, less strong flap
// Flap method
self.flap = function () {
self.vy = self.flapStrength;
LK.getSound('flap').play();
};
// Toggle drink method
self.toggleDrink = function () {
// Cycle through no drink, bird drink, drink 2, and drink 3
if (!birdDrinkSprite.visible && !drink2Sprite.visible && !drink3Sprite.visible) {
birdDrinkSprite.visible = true;
drink2Sprite.visible = false;
drink3Sprite.visible = false;
} else if (birdDrinkSprite.visible) {
birdDrinkSprite.visible = false;
drink2Sprite.visible = true;
drink3Sprite.visible = false;
} else if (drink2Sprite.visible) {
birdDrinkSprite.visible = false;
drink2Sprite.visible = false;
drink3Sprite.visible = true;
} else {
birdDrinkSprite.visible = false;
drink2Sprite.visible = false;
drink3Sprite.visible = false;
}
// Save the current drink state to storage
storage.lastDrink = birdDrinkSprite.visible ? 1 : drink2Sprite.visible ? 2 : 0;
};
// Update method
self.update = function () {
self.vy += self.gravity * (slowTimeActive ? slowTimeMultiplier : 1);
self.y += self.vy;
// Rotate bird based on velocity (visual only)
var maxAngle = Math.PI / 4;
var minAngle = -Math.PI / 6;
var angle = self.vy / 40 * maxAngle;
if (angle > maxAngle) angle = maxAngle;
if (angle < minAngle) angle = minAngle;
birdSprite.rotation = angle;
// Keep drink upright and in front
birdDrinkSprite.rotation = 0;
};
return self;
});
// Fire class for standalone fire effects
var Fire = Container.expand(function () {
var self = Container.call(this);
var fireSprite = self.attachAsset('fire', {
anchorX: 0.5,
anchorY: 0.5,
visible: false,
alpha: 0.5
});
self.shootFire = function () {
fireSprite.visible = true;
fireSprite.x = 0; // Reset position when shooting
fireSprite.y = 0;
tween(fireSprite, {
alpha: 1
}, {
duration: 500,
easing: tween.linear,
onFinish: function onFinish() {
tween(fireSprite, {
alpha: 0.5
}, {
duration: 500,
easing: tween.linear
});
}
});
};
self.getBoundingRect = function () {
if (!fireSprite.visible) return null;
return {
x: self.x + fireSprite.x - fireSprite.width * 0.5,
y: self.y + fireSprite.y - fireSprite.height * 0.5,
width: fireSprite.width,
height: fireSprite.height
};
};
self.update = function () {
if (fireSprite.visible) {
fireSprite.x -= pipeSpeed;
if (fireSprite.x < -fireSprite.width) {
fireSprite.visible = false;
}
}
};
return self;
});
var FirePipe = Container.expand(function () {
var self = Container.call(this);
var firePipeSprite = self.attachAsset('FirePipe', {
anchorX: 0.5,
anchorY: 0.5,
visible: true,
alpha: 1
});
self.movingRight = true; // Track movement direction
self.startMovement = function () {
// Start continuous left-right movement
self.moveLeftRight();
};
self.moveLeftRight = function () {
// Check if this pipe is frozen
if (self.frozen) return;
var targetX = self.movingRight ? 300 : -300; // Move 300px in each direction
tween(firePipeSprite, {
x: targetX
}, {
duration: 2000,
// 2 seconds to move in one direction
easing: tween.easeInOut,
onFinish: function onFinish() {
// Reverse direction and continue moving
self.movingRight = !self.movingRight;
self.moveLeftRight();
}
});
};
self.freeze = function () {
self.frozen = true;
// Add visual effect for frozen state
tween(firePipeSprite, {
tint: 0x88ccff
}, {
duration: 300,
easing: tween.easeOut
});
};
self.unfreeze = function () {
self.frozen = false;
// Remove visual effect
tween(firePipeSprite, {
tint: 0xffffff
}, {
duration: 300,
easing: tween.easeOut
});
// Resume movement
self.moveLeftRight();
};
self.getBoundingRect = function () {
if (!firePipeSprite.visible) return null;
return {
x: self.x + firePipeSprite.x - firePipeSprite.width * 0.5,
y: self.y + firePipeSprite.y - firePipeSprite.height * 0.5,
width: firePipeSprite.width,
height: firePipeSprite.height
};
};
self.update = function () {
// Movement is now handled by tween
};
return self;
});
// PipePair class (top and bottom pipes)
var PipePair = Container.expand(function () {
var self = Container.call(this);
// Pipe config
var pipeWidth = 200;
var pipeHeight = 1200;
var gapHeight = 480; // vertical gap between pipes
// Randomly decide if pipes are flipped (top/bottom swap orientation)
var flip = Math.random() < 0.5;
// Randomly decide pipe type: normal (70%) or fire (30%)
var isFirePipe = Math.random() < 0.3;
// For fire pipes, always single pipe. For regular pipes, 30% chance of single pipe
var isSinglePipe = isFirePipe || Math.random() < 0.3;
var pipeAsset = isFirePipe ? 'FirePipe' : 'pipe';
var topPipe = null;
var bottomPipe = null;
// Create pipes based on configuration
if (isSinglePipe) {
// Single pipe - randomly choose top or bottom position
var isTopPosition = Math.random() < 0.5;
if (isTopPosition) {
topPipe = self.attachAsset(pipeAsset, {
anchorX: 0,
anchorY: flip ? 0 : 1,
width: pipeWidth,
height: pipeHeight,
rotation: flip ? Math.PI : 0
});
} else {
bottomPipe = self.attachAsset(pipeAsset, {
anchorX: 0,
anchorY: flip ? 1 : 0,
width: pipeWidth,
height: pipeHeight,
rotation: flip ? Math.PI : 0
});
}
} else {
// Double pipes (traditional setup)
topPipe = self.attachAsset(pipeAsset, {
anchorX: 0,
anchorY: flip ? 0 : 1,
width: pipeWidth,
height: pipeHeight,
rotation: flip ? Math.PI : 0
});
bottomPipe = self.attachAsset(pipeAsset, {
anchorX: 0,
anchorY: flip ? 1 : 0,
width: pipeWidth,
height: pipeHeight,
rotation: flip ? Math.PI : 0
});
}
self.pipeWidth = pipeWidth;
self.gapHeight = gapHeight;
self.passed = false; // for scoring
self.isFirePipe = isFirePipe;
self.isSinglePipe = isSinglePipe;
self.fireTimer = 0;
self.topPipe = topPipe;
self.bottomPipe = bottomPipe;
// Set pipes' vertical positions based on gapY (top of gap)
self.setGap = function (gapY) {
// gapY: y position of top of gap
if (topPipe) topPipe.y = gapY;
if (bottomPipe) bottomPipe.y = gapY + gapHeight;
};
// Move pipes left
self.update = function () {
self.x -= pipeSpeed;
};
// Get bounding rects for collision
self.getTopRect = function () {
if (!topPipe) return null;
return {
x: self.x,
y: topPipe.y - pipeHeight,
width: pipeWidth,
height: pipeHeight
};
};
self.getBottomRect = function () {
if (!bottomPipe) return null;
return {
x: self.x,
y: bottomPipe.y,
width: pipeWidth,
height: pipeHeight
};
};
// Add fire shooting capability for fire pipes
if (isFirePipe) {
var firePipe = new FirePipe();
self.addChild(firePipe);
self.firePipe = firePipe;
self.fireStarted = false; // Track if fire has been started
}
self.update = function () {
self.x -= pipeSpeed * (slowTimeActive ? slowTimeMultiplier : 1);
// Update fire pipe if this is a fire pipe
if (self.isFirePipe && self.firePipe) {
self.firePipe.x = self.x; // Position fire at pipe position
// Center fire in the gap or at pipe level for single pipes
var fireY = bottomPipe ? bottomPipe.y - gapHeight / 2 : topPipe ? topPipe.y + gapHeight / 2 : 1366;
self.firePipe.y = fireY;
self.firePipe.update();
// Start movement when pipe enters screen (only once)
if (!self.fireStarted && self.x < 2048) {
self.fireStarted = true;
// Check if power-up is active and freeze immediately
if (slowTimeActive) {
self.firePipe.freeze();
frozenPipes.push(self.firePipe);
} else {
self.firePipe.startMovement();
}
}
}
};
return self;
});
// PowerUp class
var PowerUp = Container.expand(function () {
var self = Container.call(this);
// Create a star-shaped power-up visual using new asset
var powerUpSprite = self.attachAsset('powerUpStar', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x00ffff // Cyan color for freeze power-up
});
// Add pulsing effect
self.pulse = function () {
tween(powerUpSprite, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(powerUpSprite, {
scaleX: 1,
scaleY: 1
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
self.pulse();
}
});
}
});
};
self.collected = false;
self.update = function () {
if (!self.collected) {
self.x -= pipeSpeed * (slowTimeActive ? slowTimeMultiplier : 1);
}
};
self.getBoundingRect = function () {
return {
x: self.x - powerUpSprite.width * 0.5,
y: self.y - powerUpSprite.height * 0.5,
width: powerUpSprite.width,
height: powerUpSprite.height
};
};
// Start pulsing animation
self.pulse();
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87ceeb // sky blue
});
/****
* Game Code
****/
// Sound: Hit
// Sound: Score
// Sound: Flap
// Ground: brown box
// Pipe: green box
// Bird: yellow ellipse
// Game constants
var BIRD_START_X = 600;
var BIRD_START_Y = 1200;
var PIPE_INTERVAL = 1300; // px between pipes horizontally (slower spawn rate)
var PIPE_MIN_Y = 350; // min y for top of gap
var PIPE_MAX_Y = 1800; // max y for top of gap
var GROUND_Y = 2732 - 120; // ground top y
var pipeSpeed = 8; // px per frame (slower pipes)
var POWERUP_DURATION = 30000; // 30 seconds in milliseconds
var slowTimeMultiplier = 0.3; // 30% speed during slow time
// Game state
var bird;
var pipes = [];
var powerUps = [];
var ground;
var score = 0;
var scoreTxt;
var gameOver = false;
var started = false;
var lastPipeX = 0;
var slowTimeActive = false;
var slowTimeTimer = null;
var frozenPipes = [];
// GUI: Score
scoreTxt = new Text2('0', {
size: 160,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// GUI: Game Over text
var gameOverTxt = new Text2('Game Over', {
size: 180,
fill: 0xFF4444
});
gameOverTxt.anchor.set(0.5, 0.5);
gameOverTxt.visible = false;
LK.gui.center.addChild(gameOverTxt);
// GUI: Tap to restart
var restartTxt = new Text2('Tap to restart', {
size: 100,
fill: 0xFFFFFF
});
restartTxt.anchor.set(0.5, 0.5);
restartTxt.visible = false;
LK.gui.center.addChild(restartTxt);
// GUI: Drink toggle button
var drinkToggleBtn = new Container();
var drinkBtnBg = drinkToggleBtn.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 150,
tint: 0x4444ff
});
var drinkBtnText = new Text2('🥤', {
size: 80,
fill: 0xFFFFFF
});
drinkBtnText.anchor.set(0.5, 0.5);
drinkToggleBtn.addChild(drinkBtnText);
drinkToggleBtn.x = -100;
drinkToggleBtn.y = 100;
LK.gui.topRight.addChild(drinkToggleBtn);
// GUI: Reset score button
var resetScoreBtn = new Container();
var resetBtnBg = resetScoreBtn.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 150,
tint: 0xff4444
});
var resetBtnText = new Text2('Ø', {
size: 100,
fill: 0xFFFFFF
});
resetBtnText.anchor.set(0.5, 0.5);
resetScoreBtn.addChild(resetBtnText);
resetScoreBtn.x = -100;
resetScoreBtn.y = 280;
LK.gui.topRight.addChild(resetScoreBtn);
// GUI: Bird swapper button
var birdSwapBtn = new Container();
var birdSwapBtnBg = birdSwapBtn.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 150,
height: 150,
tint: 0x44ff44
});
var birdSwapIcon = birdSwapBtn.attachAsset('bird', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
birdSwapBtn.x = -100;
birdSwapBtn.y = 460;
LK.gui.topRight.addChild(birdSwapBtn);
// Start spinning animation for reset button
function spinResetButton() {
tween(resetBtnText, {
rotation: resetBtnText.rotation + Math.PI * 2
}, {
duration: 2000,
easing: tween.linear,
onFinish: function onFinish() {
spinResetButton();
}
});
}
spinResetButton();
// GUI: Power-up indicator
var powerUpIndicator = new Container();
var powerUpBg = powerUpIndicator.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 200,
height: 80,
tint: 0x00ffff
});
var powerUpText = new Text2('FREEZE: 30s', {
size: 40,
fill: 0xFFFFFF
});
powerUpText.anchor.set(0.5, 0.5);
powerUpIndicator.addChild(powerUpText);
powerUpIndicator.x = 0;
powerUpIndicator.y = 200;
powerUpIndicator.visible = false;
LK.gui.top.addChild(powerUpIndicator);
// Drink toggle handler
drinkToggleBtn.down = function () {
if (bird) {
bird.toggleDrink();
}
};
// Reset score handler
resetScoreBtn.down = function () {
score = 0;
scoreTxt.setText('0');
storage.lastScore = 0;
// Flash effect to indicate reset
LK.effects.flashObject(resetScoreBtn, 0xffffff, 300);
};
// Bird swap handler
birdSwapBtn.down = function () {
if (bird) {
// Get current bird type from storage or default to 'bird'
var currentBirdType = storage.currentBirdType || 'bird';
var newBirdType = currentBirdType === 'bird' ? 'Bird2' : 'bird';
// Store current drink state
var drinkState = {
drink1: bird.children[1].visible,
drink2: bird.children[2].visible,
drink3: bird.children[3].visible
};
// Remove current main bird sprite
var currentBird = bird.children[0];
bird.removeChild(currentBird);
// Create new bird sprite
var newBird = LK.getAsset(newBirdType, {
anchorX: 0.5,
anchorY: 0.5
});
bird.addChildAt(newBird, 0);
bird.currentSprite = newBird;
// Update rotation to match current velocity
var maxAngle = Math.PI / 4;
var minAngle = -Math.PI / 6;
var angle = bird.vy / 40 * maxAngle;
if (angle > maxAngle) angle = maxAngle;
if (angle < minAngle) angle = minAngle;
newBird.rotation = angle;
// Restore drink state
bird.children[1].visible = drinkState.drink1;
bird.children[2].visible = drinkState.drink2;
bird.children[3].visible = drinkState.drink3;
// Save new bird type
storage.currentBirdType = newBirdType;
// Flash effect to indicate swap
LK.effects.flashObject(birdSwapBtn, 0xffffff, 300);
}
};
// Add ground
ground = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0,
x: 0,
y: GROUND_Y
});
game.addChild(ground);
// Helper: Reset game state
function resetGame() {
// Remove pipes
for (var i = 0; i < pipes.length; i++) {
pipes[i].destroy();
}
pipes = [];
// Remove power-ups
for (var i = 0; i < powerUps.length; i++) {
powerUps[i].destroy();
}
powerUps = [];
// Clear power-up state
if (slowTimeTimer) {
LK.clearTimeout(slowTimeTimer);
slowTimeTimer = null;
}
slowTimeActive = false;
powerUpIndicator.visible = false;
frozenPipes = [];
// Remove bird if exists
if (bird) bird.destroy();
// Reset variables
score = 0;
scoreTxt.setText('0');
gameOver = false;
started = false;
lastPipeX = 0;
// Hide game over
gameOverTxt.visible = false;
restartTxt.visible = false;
// Add bird
bird = new Bird();
bird.x = BIRD_START_X;
bird.y = BIRD_START_Y;
game.addChild(bird);
// Restore score if available
if (typeof storage.lastScore !== "undefined") {
score = storage.lastScore;
scoreTxt.setText(score + '');
}
// Restore bird type if available
if (typeof storage.currentBirdType !== "undefined" && storage.currentBirdType === 'Bird2') {
var currentBird = bird.children[0];
var newBird = LK.getAsset('Bird2', {
anchorX: 0.5,
anchorY: 0.5
});
bird.removeChild(currentBird);
bird.addChildAt(newBird, 0);
bird.currentSprite = newBird;
}
// Restore drink state if available
if (typeof storage.lastDrink !== "undefined") {
if (storage.lastDrink === 1) {
bird.children[1].visible = true;
bird.children[2].visible = false;
} else if (storage.lastDrink === 2) {
bird.children[1].visible = false;
bird.children[2].visible = true;
} else if (storage.lastDrink === 3) {
bird.children[1].visible = false;
bird.children[2].visible = false;
bird.children[3].visible = true;
} else {
bird.children[1].visible = false;
bird.children[2].visible = false;
bird.children[3].visible = false;
}
}
// Add first pipes
for (var i = 0; i < 3; i++) {
spawnPipe(2048 + i * PIPE_INTERVAL);
}
}
// Helper: Spawn a pipe pair at x
function spawnPipe(x) {
var gapY = PIPE_MIN_Y + Math.floor(Math.random() * (PIPE_MAX_Y - PIPE_MIN_Y));
var pipePair = new PipePair();
pipePair.x = x;
pipePair.setGap(gapY);
pipes.push(pipePair);
game.addChild(pipePair);
lastPipeX = x;
// 10% chance to spawn power-up with pipe
if (Math.random() < 0.1 && !slowTimeActive) {
var powerUp = new PowerUp();
powerUp.x = x + pipePair.pipeWidth / 2;
powerUp.y = gapY + pipePair.gapHeight / 2;
powerUps.push(powerUp);
game.addChild(powerUp);
}
}
// Helper: Check collision between bird and a pipe rect
function birdHitsRect(rect) {
// Return false if rect is null (e.g., when fire sprite is not visible)
if (!rect) return false;
// Bird is a circle, rect is {x, y, width, height}
var cx = bird.x;
var cy = bird.y;
var r = bird.radius;
// Find closest point in rect to bird center
var closestX = cx;
if (cx < rect.x) closestX = rect.x;else if (cx > rect.x + rect.width) closestX = rect.x + rect.width;
var closestY = cy;
if (cy < rect.y) closestY = rect.y;else if (cy > rect.y + rect.height) closestY = rect.y + rect.height;
var dx = cx - closestX;
var dy = cy - closestY;
return dx * dx + dy * dy < r * r;
}
// Helper: Activate power-up
function activatePowerUp() {
slowTimeActive = true;
powerUpIndicator.visible = true;
// Freeze all fire pipes
for (var i = 0; i < pipes.length; i++) {
if (pipes[i].isFirePipe && pipes[i].firePipe) {
pipes[i].firePipe.freeze();
frozenPipes.push(pipes[i].firePipe);
}
}
// Start countdown
var timeLeft = POWERUP_DURATION / 1000;
var countdownInterval = LK.setInterval(function () {
timeLeft--;
powerUpText.setText('FREEZE: ' + timeLeft + 's');
if (timeLeft <= 0) {
LK.clearInterval(countdownInterval);
}
}, 1000);
// Set timer to deactivate power-up
if (slowTimeTimer) {
LK.clearTimeout(slowTimeTimer);
}
slowTimeTimer = LK.setTimeout(function () {
slowTimeActive = false;
powerUpIndicator.visible = false;
// Unfreeze all frozen pipes
for (var i = 0; i < frozenPipes.length; i++) {
frozenPipes[i].unfreeze();
}
frozenPipes = [];
LK.clearInterval(countdownInterval);
}, POWERUP_DURATION);
}
// Helper: End game
function triggerGameOver() {
if (gameOver) return;
gameOver = true;
// Save score and drink state to storage
storage.lastScore = score;
// Access birdDrinkSprite via bird instance if available
storage.lastDrink = bird && bird.hasOwnProperty('children') && bird.children.length > 1 ? bird.children[1].visible ? 1 : bird.children[2].visible ? 2 : bird.children[3].visible ? 3 : 0 : 0;
LK.getSound('hit').play();
gameOverTxt.visible = true;
restartTxt.visible = true;
// Flash screen
LK.effects.flashScreen(0xff0000, 600);
// Show Game Over popup (will pause game)
// LK.showGameOver();
}
// Game tap/flap handler
function handleTap(x, y, obj) {
if (gameOver) {
resetGame();
return;
}
if (!started) {
started = true;
}
bird.flap();
}
// Attach tap handler
game.down = handleTap;
// Main update loop
game.update = function () {
if (gameOver) return;
// Bird update
bird.update();
// Pipes update
for (var i = pipes.length - 1; i >= 0; i--) {
var pipe = pipes[i];
pipe.update();
// Remove pipes off screen
if (pipe.x + pipe.pipeWidth < 0) {
pipe.destroy();
pipes.splice(i, 1);
continue;
}
// Scoring: passed pipe
if (!pipe.passed && pipe.x + pipe.pipeWidth < bird.x) {
pipe.passed = true;
score += 1;
scoreTxt.setText(score + '');
LK.getSound('score').play();
}
}
// Spawn new pipes
if (pipes.length > 0) {
var rightmost = pipes[pipes.length - 1];
if (2048 - (rightmost.x + rightmost.pipeWidth) >= PIPE_INTERVAL - 10) {
spawnPipe(2048);
}
}
// Power-ups update and collision
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
powerUp.update();
// Remove power-ups off screen
if (powerUp.x + 40 < 0) {
powerUp.destroy();
powerUps.splice(i, 1);
continue;
}
// Check collision with bird
if (!powerUp.collected && birdHitsRect(powerUp.getBoundingRect())) {
powerUp.collected = true;
powerUp.visible = false;
activatePowerUp();
LK.getSound('score').play();
}
}
// Collision: pipes
for (var i = 0; i < pipes.length; i++) {
var pipe = pipes[i];
var hitTopPipe = birdHitsRect(pipe.getTopRect());
var hitBottomPipe = birdHitsRect(pipe.getBottomRect());
var hitPipe = hitTopPipe || hitBottomPipe;
var hitFire = pipe.isFirePipe && pipe.firePipe && pipe.firePipe.getBoundingRect && birdHitsRect(pipe.firePipe.getBoundingRect());
if (hitPipe || hitFire) {
triggerGameOver();
return;
}
}
// Collision: ground
if (bird.y + bird.radius > GROUND_Y) {
bird.y = GROUND_Y - bird.radius;
triggerGameOver();
return;
}
// Collision: ceiling
if (bird.y - bird.radius < 0) {
bird.y = bird.radius;
bird.vy = 0;
}
// Out of bounds (left/right)
if (bird.x - bird.radius < 0 || bird.x + bird.radius > 2048) {
triggerGameOver();
return;
}
};
// Initial game state
resetGame();
Pipe. In-Game asset. 2d. High contrast. No shadows
Bird. In-Game asset. 2d. High contrast. No shadows
Rock bg. In-Game asset. 2d. High contrast. No shadows
Cup with Straw. In-Game asset. 2d. High contrast. No shadows
Coke bottle with straw. In-Game asset. 2d. High contrast. No shadows
Fire. In-Game asset. 2d. High contrast. No shadows
Red pipe. In-Game asset. 2d. High contrast. No shadows