/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
var NonPerfectMessage = Container.expand(function () {
var self = Container.call(this);
var messageText = new Text2('Bad timing', {
size: 120,
fill: 0xFF0000,
stroke: 0xffffff,
strokeThickness: 8
});
messageText.anchor.set(0.5, 0.5);
self.addChild(messageText);
self.alpha = 1;
self.scale.set(0.1);
self.lifespan = 60; // 1 second at 60fps
self.initialized = false;
self.creationTime = Date.now(); // Track when message was created
self.init = function () {
if (self.initialized) {
return;
}
self.initialized = true;
// First tween: grow from small to large
tween(self.scale, {
x: 1.0,
y: 1.0
}, {
duration: 300,
easing: tween.easeOut,
onComplete: function onComplete() {
// Second tween: wait a bit then fade out
LK.setTimeout(function () {
tween(self, {
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onComplete: function onComplete() {
// Find and remove from perfectMessages array
var index = perfectMessages.indexOf(self);
if (index > -1) {
perfectMessages.splice(index, 1);
}
// Remove from game first before destroying
if (self.parent) {
self.parent.removeChild(self);
}
self.destroy();
}
});
}, 600);
}
});
};
self.update = function () {
if (!self.initialized) {
self.init();
}
// Manual animation as fallback if tweens fail
if (!self.initialized) {
self.lifespan--;
if (self.lifespan <= 0) {
// Find and remove from perfectMessages array
var index = perfectMessages.indexOf(self);
if (index > -1) {
perfectMessages.splice(index, 1);
}
self.destroy();
return;
}
// Animation effect
if (self.lifespan > 45) {
// Growing phase
self.scale.set(0.1 + (1.0 - 0.1) * (1 - (self.lifespan - 45) / 15));
} else if (self.lifespan < 15) {
// Fading out phase
self.alpha = self.lifespan / 15;
}
}
};
return self;
});
var Palm = Container.expand(function () {
var self = Container.call(this);
var palmGraphics = self.attachAsset('palm', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = palmGraphics.width;
self.height = palmGraphics.height;
self.isMoving = false;
self.direction = "left"; // Default direction
self.lastY = 0;
self.update = function () {
// Animation will be handled by tweens
// This just ensures we have an update method for tracking
};
return self;
});
var PerfectMessage = Container.expand(function () {
var self = Container.call(this);
var messageText = new Text2('PERFECT! +2!', {
size: 120,
fill: 0xFFF000,
stroke: 0xFF0000,
strokeThickness: 8
});
messageText.anchor.set(0.5, 0.5);
self.addChild(messageText);
self.alpha = 1;
self.scale.set(0.1);
self.lifespan = 60; // 1 second at 60fps
self.initialized = false;
self.creationTime = Date.now(); // Track when message was created
self.init = function () {
if (self.initialized) {
return;
}
self.initialized = true;
// First tween: grow from small to large
tween(self.scale, {
x: 1.0,
y: 1.0
}, {
duration: 300,
easing: tween.easeOut,
onComplete: function onComplete() {
// Second tween: wait a bit then fade out
LK.setTimeout(function () {
tween(self, {
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onComplete: function onComplete() {
// Find and remove from perfectMessages array
var index = perfectMessages.indexOf(self);
if (index > -1) {
perfectMessages.splice(index, 1);
}
// Remove from game first before destroying
if (self.parent) {
self.parent.removeChild(self);
}
self.destroy();
}
});
}, 600);
}
});
};
self.update = function () {
if (!self.initialized) {
self.init();
}
// Manual animation as fallback if tweens fail
if (!self.initialized) {
self.lifespan--;
if (self.lifespan <= 0) {
// Find and remove from perfectMessages array
var index = perfectMessages.indexOf(self);
if (index > -1) {
perfectMessages.splice(index, 1);
}
self.destroy();
return;
}
// Animation effect
if (self.lifespan > 45) {
// Growing phase
self.scale.set(0.1 + (1.0 - 0.1) * (1 - (self.lifespan - 45) / 15));
} else if (self.lifespan < 15) {
// Fading out phase
self.alpha = self.lifespan / 15;
}
}
};
return self;
});
var PlayerCar = Container.expand(function () {
var self = Container.call(this);
var carGraphics = self.attachAsset('playerCar', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = carGraphics.width;
self.height = carGraphics.height;
self.speed = 0;
self.maxSpeed = 15;
self.steering = 0;
self.crashed = false;
self.lastX = 0;
self.bubble = null;
self.crash = function () {
if (!self.crashed) {
self.crashed = true;
// 1. First, play the crash sound
LK.getSound('crash').play();
// 2. Second, mirror vertically the player Car
carGraphics.scaleY = -1;
// Flash effect for visual feedback
LK.effects.flashObject(self, 0xFF0000, 1000);
// 3. Stop the game by setting speed to 0
self.speed = 0;
// 4. After 2 seconds, show GameOver
LK.setTimeout(function () {
LK.showGameOver();
}, 2000);
}
};
self.update = function () {
// Save last position for movement detection
self.lastX = self.x;
// Handle car physics
if (!self.crashed) {
self.speed = Math.min(self.maxSpeed, self.speed + 0.1);
}
// Apply steering if not crashed
if (!self.crashed) {
self.x += self.steering;
}
// Keep car within bounds
if (self.x < self.width / 2) {
self.x = self.width / 2;
}
if (self.x > 2048 - self.width / 2) {
self.x = 2048 - self.width / 2;
}
// Check if car has moved horizontally and has a bubble
if (Math.abs(self.x - self.lastX) > 0.1 && self.bubble && self.bubble.parent) {
// Remove bubble immediately when car moves
if (self.bubble.parent) {
self.bubble.parent.removeChild(self.bubble);
}
self.bubble.destroy();
self.bubble = null;
}
};
return self;
});
var RoadLine = Container.expand(function () {
var self = Container.call(this);
var lineGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = lineGraphics.width;
self.height = lineGraphics.height;
self.isMoving = false;
self.lastY = 0;
self.update = function () {
// Animation will be handled by tweens
// This just ensures we have an update method for tracking
};
return self;
});
// RoadLine class removed as requested
var TrafficCar = Container.expand(function () {
var self = Container.call(this);
// Randomly select one of the available traffic car assets
var carAssets = ['trafficCar', 'trafficCar2', 'trafficCar3', 'trafficCar4'];
var selectedCarAsset = carAssets[Math.floor(Math.random() * carAssets.length)];
self.direction = Math.random() > 0.5 ? 1 : -1; // Random direction: -1 = left, 1 = right
var carGraphics = self.attachAsset(selectedCarAsset, {
anchorX: 0.5,
anchorY: 0.5,
x: self.direction == -1 ? 0 : 200
});
self.width = carGraphics.width;
self.height = carGraphics.height;
self.speed = 0.1 * level * SPEED_LEVEL_FACTOR;
self.direction = Math.random() > 0.5 ? 1 : -1; // Random direction: -1 = left, 1 = right
self.startY = 1300; // Starting Y position (further up the screen)
self.targetY = 2732 + self.height; // Y position when fully off-screen (bottom)
self.initialScale = 0.1;
self.targetScale = 2.0;
// Set target X position in one of the specified ranges
self.targetX = Math.random() > 0.5 ? Math.random() * 500 + 2000 : Math.random() * 100;
// Set initial scale
self.scale.set(self.initialScale);
self.update = function () {
// Calculate progress towards target Y
var progress = (self.y - self.startY) / (self.targetY - self.startY);
progress = Math.max(0, Math.min(1, progress)); // Clamp between 0 and 1
// Interpolate scale based on progress
var currentScale = self.initialScale + (self.targetScale - self.initialScale) * progress;
self.scale.set(currentScale);
// Calculate X position based on progress (linear interpolation)
var startX = 2048 / 2; // Center of screen
self.x = startX + (self.targetX - startX) * progress;
// Apply level-based speed increase
self.speed += progress * level * SPEED_LEVEL_FACTOR;
// Update Y position with speed that increases as the car grows
self.y += self.speed;
// Destroy if off screen
if (self.y > self.targetY) {
// Check against targetY
// Find and remove self from trafficCars array
var index = trafficCars.indexOf(self);
if (index > -1) {
trafficCars.splice(index, 1);
// Check for perfect timing - if honk was within 1 second (1000ms) before or after car disappears
var currentTime = Date.now();
var timeDifference = Math.abs(currentTime - lastHonkTime);
if (timeDifference <= 500 * 2 / Math.min(3, level)) {
// Perfect timing! Add 2 extra points
LK.setScore(LK.getScore() + 3); // +1 for normal disappearance, +2 for perfect timing
// Create and display perfect message
var perfectMsg = new PerfectMessage();
perfectMsg.x = centerX;
perfectMsg.y = centerY;
perfectMsg.creationTime = Date.now(); // Set creation time
game.addChild(perfectMsg);
perfectMessages.push(perfectMsg);
// Explicitly call init to ensure animation starts immediately
perfectMsg.init();
} else if (timeDifference > 500 * 2 / Math.min(3, level) && lastHonkTime > 0 && lastHonkTime > currentTime - 3000) {
// Bad timing! Show a non-perfect message only if honk was played AFTER this car crossed
// (checking if honk happened within last 3 seconds to ensure it relates to this car)
var nonPerfectMsg = new NonPerfectMessage();
nonPerfectMsg.x = centerX;
nonPerfectMsg.y = centerY;
nonPerfectMsg.creationTime = Date.now(); // Set creation time
game.addChild(nonPerfectMsg);
perfectMessages.push(nonPerfectMsg); // Add to the same array for cleanup
// Explicitly call init to ensure animation starts immediately
nonPerfectMsg.init();
// Normal score increment
LK.setScore(LK.getScore() + 1);
} else {
// Normal score increment
LK.setScore(LK.getScore() + 1);
}
scoreText.setText(LK.getScore().toString());
scoreText.setStyle({
tint: 0xffffff,
stroke: 0xffffff,
strokeThickness: 5
});
// List of available sounds excluding crash
var sounds = ['blind', 'getOff', 'holdingTraffic', 'learnToDrive', 'license', 'moveOver', 'speedUp', 'whatAreYouDoing'];
// Get current last played sound from game variable or initialize it
var lastPlayedSound = game.lastPlayedSound || '';
// Filter out the last played sound if it exists
var availableSounds = sounds.filter(function (sound) {
return sound !== lastPlayedSound;
});
// Select a random sound from the filtered list
var randomSound = availableSounds[Math.floor(Math.random() * availableSounds.length)];
// Store the selected sound as the last played sound
game.lastPlayedSound = randomSound;
// Play the selected sound
LK.getSound(randomSound).play();
// Check if player already has a bubble
if (player.bubble && player.bubble.parent) {
player.bubble.parent.removeChild(player.bubble);
player.bubble.destroy();
}
// Show bubble over player car
var bubble = LK.getAsset('bubble', {
anchorX: 0.5,
anchorY: 1.0,
x: player.x,
y: player.y - player.height / 2
});
game.addChild(bubble);
bubble.scale.set(0);
// Store reference to the bubble in player object
player.bubble = bubble;
// Store creation time for auto-cleanup
bubble.creationTime = Date.now();
// Animate the bubble appearing and disappearing
tween(bubble.scale, {
x: 0.8,
y: 0.8
}, {
duration: 200,
easing: tween.easeOut,
onComplete: function onComplete() {
// Wait for 0.3 seconds then fade out (this happens if no movement removed it)
LK.setTimeout(function () {
// Check if bubble still exists and hasn't been removed by movement
if (bubble && bubble.parent) {
tween(bubble, {
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onComplete: function onComplete() {
if (bubble.parent) {
bubble.parent.removeChild(bubble);
}
bubble.destroy();
// Clear reference in player
if (player.bubble === bubble) {
player.bubble = null;
}
}
});
}
}, 800); // Increased to ensure bubble shows for 1 second total
}
});
}
self.destroy();
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game state variables
var gameStarted = false;
var gameTime = 30; // Initial time in seconds
var distance = 0;
var level = 1;
var checkpointDistance = 500; // Distance between checkpoints
var nextCheckpointAt = checkpointDistance;
var trafficCars = [];
var obstacles = [];
var checkpoints = [];
var timeBonuses = [];
var difficultyTimer;
var lastHonkTime = 0; // Track when player last honked
var perfectMessages = []; // Array to store perfect timing messages
// Create container groups
var palmGroup = new Container();
var roadLinesGroup = new Container();
var SPEED_LEVEL_FACTOR = 3;
// Create road lines
var ROAD_LINE_COUNT = 15;
var roadLines = [];
var BASE_ROAD_LINE_COOLDOWN = 20; // Base spawn cooldown
var roadLineSpawnCooldown = 20; // Spawn a road line every 0.33 seconds if needed
var roadLineSpawnTimer = 0; // Timer to track cooldown
// Create palm trees
var PALM_COUNT = 40;
var palms = [];
var centerX = 2048 / 2; // Center of screen X
var centerY = 2732 / 2; // Center of screen Y
var BASE_PALM_COOLDOWN = 10; // Base spawn cooldown
var palmSpawnCooldown = 10; // Spawn a palm every 0.5 seconds if needed
var palmSpawnTimer = 0; // Timer to track cooldown
var lastSpawnSide = "right"; // Track the last side a palm was spawned on
// Sway parameters
var swayCounter = 0;
// Create road
var road = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2 + 50,
y: 2732 / 2 - 100
// y: 1000
});
game.addChild(road);
game.addChild(palmGroup);
game.addChild(roadLinesGroup);
// Road lines removed as requested
// Create player car
var player = new PlayerCar();
player.x = 2048 / 2;
player.y = 2732 - 300;
game.addChild(player);
var scoreLabel = new Text2('Score:', {
size: 80,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 5
});
scoreLabel.anchor.set(0, 0);
scoreLabel.x = -150;
LK.gui.top.addChild(scoreLabel);
var scoreText = new Text2('0', {
size: 80,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 5
});
scoreText.anchor.set(0, 0);
scoreText.x = 100;
LK.gui.top.addChild(scoreText);
var levelLabel = new Text2('Level:', {
size: 80,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 5
});
levelLabel.anchor.set(0, 0);
levelLabel.x = -150;
levelLabel.y = 100; // Offset below time
LK.gui.top.addChild(levelLabel);
var levelText = new Text2('1', {
size: 80,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 5
});
levelText.anchor.set(0, 0);
levelText.x = 100;
levelText.y = 100; // Offset below time
LK.gui.top.addChild(levelText);
var startText = new Text2('TAP TO START\nTAP TO 🔊\nPERFECTLY TIMED 🔊 = +2', {
size: 120,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 8,
align: 'center'
});
startText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(startText);
// Functions to spawn game elements
function spawnTrafficCar() {
// Only spawn a new car if there are no cars on screen
if (trafficCars.length === 0) {
var car = new TrafficCar();
car.x = 2048 / 2; // Start in the middle of the screen
car.y = car.startY; // Start at the defined startY
car.speed = 0.1 * level * SPEED_LEVEL_FACTOR;
trafficCars.push(car);
game.addChild(car);
}
// Schedule next check for spawning with faster spawns at higher levels
var nextSpawnTime = Math.max(200, 1000 / level); // Faster spawns at higher levels
LK.setTimeout(spawnTrafficCar, nextSpawnTime);
}
// Start game function
function startGame() {
gameStarted = true;
gameTime = 30;
distance = 0;
level = 1; // Initial level starts at 1
nextCheckpointAt = checkpointDistance;
LK.setScore(0);
// Initialize road lines with pre-animated positions as if they had already been running for 10 seconds
roadLines = [];
for (var i = 0; i < roadLinesGroup.children.length; i++) {
roadLinesGroup.children[i].destroy();
}
roadLinesGroup.removeChildren();
roadLineSpawnTimer = 0;
// Create pre-existing road lines as if they were already running for 10 seconds
for (var i = 0; i < ROAD_LINE_COUNT; i++) {
var newRoadLine = new RoadLine();
// Calculate a position along the animation path as if it had been running
// Progress ranges from 0 (just started) to 1 (almost finished)
var progress = i / ROAD_LINE_COUNT;
// Start at center X and interpolate Y based on progress
newRoadLine.x = centerX + 15;
// Calculate Y position - from center-50 to beyond bottom of screen
var startY = centerY - 50;
var targetY = 2732 + 100;
newRoadLine.y = startY + (targetY - startY) * progress;
// Calculate scale based on progress (0.1 to 1.0)
var startScale = 0.1;
var endScale = 1.0;
var currentScale = startScale + (endScale - startScale) * progress;
newRoadLine.scale.set(currentScale);
newRoadLine.lastY = newRoadLine.y;
newRoadLine.isMoving = true;
// Only add the road line and start animation if it's still on screen
if (newRoadLine.y < 2732) {
// Create tween animation with perspective effect - already in progress
var remainingDuration = 4000 * (1 - progress) / level;
tween(newRoadLine, {
y: targetY,
scaleX: endScale,
scaleY: endScale
}, {
duration: remainingDuration,
easing: tween.easeIn
});
roadLines.push(newRoadLine);
roadLinesGroup.addChild(newRoadLine);
} else {
newRoadLine.destroy();
}
}
// Update UI
scoreText.setText('0');
levelText.setText(level.toString());
levelText.setStyle({
tint: 0xffffff,
stroke: 0xffffff,
strokeThickness: 5
});
// Remove start text
LK.gui.center.removeChild(startText);
// Start spawning game elements
spawnTrafficCar();
// Level is now calculated based on score/10+1, no need for a timer to increase it
// Play background music
LK.playMusic('bgmusic');
// Palm spawning is handled in the update function
}
// Handle touch input
game.down = function (x, y) {
// Only play honk sound if game has started
if (!gameStarted) {
startGame();
return;
}
// Play honk sound when screen is clicked/tapped after game has started
LK.getSound('honk').play();
// Record the time of honk for perfect timing detection
lastHonkTime = Date.now();
// Remove any existing bubble when player honks
if (player.bubble && player.bubble.parent) {
player.bubble.parent.removeChild(player.bubble);
player.bubble.destroy();
player.bubble = null;
}
};
game.move = function (x, y) {
if (!gameStarted) {
return;
}
// Move player car directly to mouse X position
var targetX = x;
// Apply smooth movement by calculating the difference
var deltaX = targetX - player.x;
player.steering = deltaX * 0.1; // Smooth movement factor
};
game.up = function () {
if (gameStarted) {
// No need to reset steering as we'll continue to follow mouse
}
};
// Main game update function
game.update = function () {
if (!gameStarted) {
return;
}
// Update road lines
if (gameStarted) {
// Update road line animations
for (var r = roadLines.length - 1; r >= 0; r--) {
var roadLine = roadLines[r];
// Check if roadLine exists before accessing its properties
if (roadLine) {
roadLine.lastY = roadLine.y;
// Start a new roadLine animation if roadLine is not already moving
if (roadLine.isMoving === false) {
roadLine.isMoving = true;
// Set target position - straight down the screen
var targetY = 2732 + 100; // Below bottom of screen
var targetScale = 1.0; // End larger
// Create tween animation with perspective effect
tween(roadLine, {
y: targetY,
scaleX: targetScale,
scaleY: targetScale
}, {
duration: 4000 / level,
easing: tween.easeIn
});
}
// Check if roadLine has moved significantly past the bottom of the screen and destroy it
var destroyYPosition = 2732 - 300; // Check if center is below the bottom edge
if (roadLine.isMoving && roadLine.y > destroyYPosition) {
// RoadLine is off-screen, destroy it
var index = roadLines.indexOf(roadLine);
if (index > -1) {
roadLinesGroup.removeChild(roadLine);
roadLines.splice(index, 1);
roadLine.destroy();
}
// Skip to next roadLine as this one is destroyed
continue;
}
}
} // End of loop processing existing road lines
// Calculate spawn cooldown based on level
roadLineSpawnCooldown = Math.max(5, BASE_ROAD_LINE_COOLDOWN / level);
// Update road line spawn timer and generate new road lines if needed
roadLineSpawnTimer -= 1;
if (roadLineSpawnTimer <= 0 && roadLines.length < ROAD_LINE_COUNT) {
roadLineSpawnTimer = roadLineSpawnCooldown; // Reset timer
var newRoadLine = new RoadLine();
// Start at center X and at center Y
newRoadLine.x = centerX + 15;
newRoadLine.y = centerY - 50;
newRoadLine.scale.set(0.1); // Start small
newRoadLine.lastY = newRoadLine.y;
newRoadLine.isMoving = false; // Flag to track if road line is currently animating
roadLines.push(newRoadLine);
roadLinesGroup.addChild(newRoadLine);
}
}
// Road lines removed as requested
// Update game time
gameTime -= 1 / 60; // Assuming 60 FPS
// Calculate level based on score/10, but minimum of 1
level = Math.floor(LK.getScore() / 10) + 1;
levelText.setText(level.toString());
// Update palm animations section
if (gameStarted) {
distance += 10 + level;
// Don't update score based on distance anymore
// Keep updating the distance for internal calculations if needed
// Update palm animations
for (var p = palms.length - 1; p >= 0; p--) {
var palm = palms[p];
// Check if palm exists before accessing its properties
if (palm) {
palm.lastY = palm.y;
// Start a new palm animation if palm is not already moving
if (palm.isMoving === false) {
palm.isMoving = true;
// Set target position based on direction (left or right corner)
var targetX = palm.direction === "left" ? -12000 : 14500; // Left or right edge
var targetY = 2732 + 100; // Below bottom of screen
var targetScale = 2.0; // End larger
// Create tween animation with perspective effect - use same duration for all palms
tween(palm, {
x: targetX,
y: targetY,
scaleX: targetScale * (lastSpawnSide === "right" ? 1 : -1),
scaleY: targetScale
}, {
duration: 7000 / level,
// Duration decreases as level increases
easing: tween.easeIn // Removed onFinish callback
});
}
// Check if palm has moved significantly past the bottom of the screen and destroy it
var destroyYPosition = 2500; // Check if center is 150px below the bottom edge
if (palm.isMoving && palm.y > destroyYPosition) {
// Palm is off-screen, destroy it
var index = palms.indexOf(palm);
if (index > -1) {
palmGroup.removeChild(palm);
palms.splice(index, 1);
palm.destroy();
}
// Skip to next palm as this one is destroyed
continue;
}
}
} // End of loop processing existing palms
// Calculate palm spawn cooldown based on level
palmSpawnCooldown = Math.max(3, BASE_PALM_COOLDOWN / level);
// Update palm spawn timer and generate new palms if needed
palmSpawnTimer -= 1;
if (palmSpawnTimer <= 0 && palms.length < PALM_COUNT) {
palmSpawnTimer = palmSpawnCooldown; // Reset timer
var newPalm = new Palm();
// Alternate spawn side based on the last spawned side
lastSpawnSide = lastSpawnSide === "right" ? "left" : "right";
// Start from exact center point
newPalm.x = centerX + (lastSpawnSide === "right" ? 50 : -50);
newPalm.y = centerY - 100; // Start at center
newPalm.scale.set(0.005); // Start small
newPalm.scaleX *= lastSpawnSide === "right" ? 1 : -1;
newPalm.lastY = newPalm.y;
newPalm.direction = lastSpawnSide; // Use the determined side
newPalm.isMoving = false; // Flag to track if palm is currently animating
palms.push(newPalm);
palmGroup.addChild(newPalm);
}
}
// Game will continue indefinitely - removed game over condition
gameTime = Math.max(gameTime, 0.1); // Keep time from reaching zero
// Update perfect messages
for (var m = perfectMessages.length - 1; m >= 0; m--) {
perfectMessages[m].update();
}
// Check for old perfect and non-perfect messages every 2 seconds
if (LK.ticks % 60 === 0) {
var currentTime = Date.now();
for (var i = perfectMessages.length - 1; i >= 0; i--) {
// If message is older than 2 seconds and still exists
if (perfectMessages[i] && perfectMessages[i].creationTime && currentTime - perfectMessages[i].creationTime > 500) {
// Remove from game if it's still attached
if (perfectMessages[i].parent) {
perfectMessages[i].parent.removeChild(perfectMessages[i]);
}
// Destroy and remove from array
perfectMessages[i].destroy();
perfectMessages.splice(i, 1);
}
}
// Check for speech bubble timing
if (player.bubble && player.bubble.creationTime && currentTime - player.bubble.creationTime > 1000) {
// Remove bubble if it's been showing for more than 1 second
if (player.bubble.parent) {
player.bubble.parent.removeChild(player.bubble);
}
player.bubble.destroy();
player.bubble = null;
}
}
// Check collisions with traffic cars
for (var i = trafficCars.length - 1; i >= 0; i--) {
var car = trafficCars[i];
if (player.intersects(car) && !player.crashed) {
player.crash();
trafficCars.splice(i, 1);
car.destroy();
// GameOver will be called after the crash sequence
}
}
};
function endGame() {
if (!gameStarted) {
return;
}
// Just save high score without ending the game
if (LK.getScore() > storage.highScore) {
storage.highScore = LK.getScore();
}
// Game continues - no game over screen
} /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
var NonPerfectMessage = Container.expand(function () {
var self = Container.call(this);
var messageText = new Text2('Bad timing', {
size: 120,
fill: 0xFF0000,
stroke: 0xffffff,
strokeThickness: 8
});
messageText.anchor.set(0.5, 0.5);
self.addChild(messageText);
self.alpha = 1;
self.scale.set(0.1);
self.lifespan = 60; // 1 second at 60fps
self.initialized = false;
self.creationTime = Date.now(); // Track when message was created
self.init = function () {
if (self.initialized) {
return;
}
self.initialized = true;
// First tween: grow from small to large
tween(self.scale, {
x: 1.0,
y: 1.0
}, {
duration: 300,
easing: tween.easeOut,
onComplete: function onComplete() {
// Second tween: wait a bit then fade out
LK.setTimeout(function () {
tween(self, {
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onComplete: function onComplete() {
// Find and remove from perfectMessages array
var index = perfectMessages.indexOf(self);
if (index > -1) {
perfectMessages.splice(index, 1);
}
// Remove from game first before destroying
if (self.parent) {
self.parent.removeChild(self);
}
self.destroy();
}
});
}, 600);
}
});
};
self.update = function () {
if (!self.initialized) {
self.init();
}
// Manual animation as fallback if tweens fail
if (!self.initialized) {
self.lifespan--;
if (self.lifespan <= 0) {
// Find and remove from perfectMessages array
var index = perfectMessages.indexOf(self);
if (index > -1) {
perfectMessages.splice(index, 1);
}
self.destroy();
return;
}
// Animation effect
if (self.lifespan > 45) {
// Growing phase
self.scale.set(0.1 + (1.0 - 0.1) * (1 - (self.lifespan - 45) / 15));
} else if (self.lifespan < 15) {
// Fading out phase
self.alpha = self.lifespan / 15;
}
}
};
return self;
});
var Palm = Container.expand(function () {
var self = Container.call(this);
var palmGraphics = self.attachAsset('palm', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = palmGraphics.width;
self.height = palmGraphics.height;
self.isMoving = false;
self.direction = "left"; // Default direction
self.lastY = 0;
self.update = function () {
// Animation will be handled by tweens
// This just ensures we have an update method for tracking
};
return self;
});
var PerfectMessage = Container.expand(function () {
var self = Container.call(this);
var messageText = new Text2('PERFECT! +2!', {
size: 120,
fill: 0xFFF000,
stroke: 0xFF0000,
strokeThickness: 8
});
messageText.anchor.set(0.5, 0.5);
self.addChild(messageText);
self.alpha = 1;
self.scale.set(0.1);
self.lifespan = 60; // 1 second at 60fps
self.initialized = false;
self.creationTime = Date.now(); // Track when message was created
self.init = function () {
if (self.initialized) {
return;
}
self.initialized = true;
// First tween: grow from small to large
tween(self.scale, {
x: 1.0,
y: 1.0
}, {
duration: 300,
easing: tween.easeOut,
onComplete: function onComplete() {
// Second tween: wait a bit then fade out
LK.setTimeout(function () {
tween(self, {
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onComplete: function onComplete() {
// Find and remove from perfectMessages array
var index = perfectMessages.indexOf(self);
if (index > -1) {
perfectMessages.splice(index, 1);
}
// Remove from game first before destroying
if (self.parent) {
self.parent.removeChild(self);
}
self.destroy();
}
});
}, 600);
}
});
};
self.update = function () {
if (!self.initialized) {
self.init();
}
// Manual animation as fallback if tweens fail
if (!self.initialized) {
self.lifespan--;
if (self.lifespan <= 0) {
// Find and remove from perfectMessages array
var index = perfectMessages.indexOf(self);
if (index > -1) {
perfectMessages.splice(index, 1);
}
self.destroy();
return;
}
// Animation effect
if (self.lifespan > 45) {
// Growing phase
self.scale.set(0.1 + (1.0 - 0.1) * (1 - (self.lifespan - 45) / 15));
} else if (self.lifespan < 15) {
// Fading out phase
self.alpha = self.lifespan / 15;
}
}
};
return self;
});
var PlayerCar = Container.expand(function () {
var self = Container.call(this);
var carGraphics = self.attachAsset('playerCar', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = carGraphics.width;
self.height = carGraphics.height;
self.speed = 0;
self.maxSpeed = 15;
self.steering = 0;
self.crashed = false;
self.lastX = 0;
self.bubble = null;
self.crash = function () {
if (!self.crashed) {
self.crashed = true;
// 1. First, play the crash sound
LK.getSound('crash').play();
// 2. Second, mirror vertically the player Car
carGraphics.scaleY = -1;
// Flash effect for visual feedback
LK.effects.flashObject(self, 0xFF0000, 1000);
// 3. Stop the game by setting speed to 0
self.speed = 0;
// 4. After 2 seconds, show GameOver
LK.setTimeout(function () {
LK.showGameOver();
}, 2000);
}
};
self.update = function () {
// Save last position for movement detection
self.lastX = self.x;
// Handle car physics
if (!self.crashed) {
self.speed = Math.min(self.maxSpeed, self.speed + 0.1);
}
// Apply steering if not crashed
if (!self.crashed) {
self.x += self.steering;
}
// Keep car within bounds
if (self.x < self.width / 2) {
self.x = self.width / 2;
}
if (self.x > 2048 - self.width / 2) {
self.x = 2048 - self.width / 2;
}
// Check if car has moved horizontally and has a bubble
if (Math.abs(self.x - self.lastX) > 0.1 && self.bubble && self.bubble.parent) {
// Remove bubble immediately when car moves
if (self.bubble.parent) {
self.bubble.parent.removeChild(self.bubble);
}
self.bubble.destroy();
self.bubble = null;
}
};
return self;
});
var RoadLine = Container.expand(function () {
var self = Container.call(this);
var lineGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = lineGraphics.width;
self.height = lineGraphics.height;
self.isMoving = false;
self.lastY = 0;
self.update = function () {
// Animation will be handled by tweens
// This just ensures we have an update method for tracking
};
return self;
});
// RoadLine class removed as requested
var TrafficCar = Container.expand(function () {
var self = Container.call(this);
// Randomly select one of the available traffic car assets
var carAssets = ['trafficCar', 'trafficCar2', 'trafficCar3', 'trafficCar4'];
var selectedCarAsset = carAssets[Math.floor(Math.random() * carAssets.length)];
self.direction = Math.random() > 0.5 ? 1 : -1; // Random direction: -1 = left, 1 = right
var carGraphics = self.attachAsset(selectedCarAsset, {
anchorX: 0.5,
anchorY: 0.5,
x: self.direction == -1 ? 0 : 200
});
self.width = carGraphics.width;
self.height = carGraphics.height;
self.speed = 0.1 * level * SPEED_LEVEL_FACTOR;
self.direction = Math.random() > 0.5 ? 1 : -1; // Random direction: -1 = left, 1 = right
self.startY = 1300; // Starting Y position (further up the screen)
self.targetY = 2732 + self.height; // Y position when fully off-screen (bottom)
self.initialScale = 0.1;
self.targetScale = 2.0;
// Set target X position in one of the specified ranges
self.targetX = Math.random() > 0.5 ? Math.random() * 500 + 2000 : Math.random() * 100;
// Set initial scale
self.scale.set(self.initialScale);
self.update = function () {
// Calculate progress towards target Y
var progress = (self.y - self.startY) / (self.targetY - self.startY);
progress = Math.max(0, Math.min(1, progress)); // Clamp between 0 and 1
// Interpolate scale based on progress
var currentScale = self.initialScale + (self.targetScale - self.initialScale) * progress;
self.scale.set(currentScale);
// Calculate X position based on progress (linear interpolation)
var startX = 2048 / 2; // Center of screen
self.x = startX + (self.targetX - startX) * progress;
// Apply level-based speed increase
self.speed += progress * level * SPEED_LEVEL_FACTOR;
// Update Y position with speed that increases as the car grows
self.y += self.speed;
// Destroy if off screen
if (self.y > self.targetY) {
// Check against targetY
// Find and remove self from trafficCars array
var index = trafficCars.indexOf(self);
if (index > -1) {
trafficCars.splice(index, 1);
// Check for perfect timing - if honk was within 1 second (1000ms) before or after car disappears
var currentTime = Date.now();
var timeDifference = Math.abs(currentTime - lastHonkTime);
if (timeDifference <= 500 * 2 / Math.min(3, level)) {
// Perfect timing! Add 2 extra points
LK.setScore(LK.getScore() + 3); // +1 for normal disappearance, +2 for perfect timing
// Create and display perfect message
var perfectMsg = new PerfectMessage();
perfectMsg.x = centerX;
perfectMsg.y = centerY;
perfectMsg.creationTime = Date.now(); // Set creation time
game.addChild(perfectMsg);
perfectMessages.push(perfectMsg);
// Explicitly call init to ensure animation starts immediately
perfectMsg.init();
} else if (timeDifference > 500 * 2 / Math.min(3, level) && lastHonkTime > 0 && lastHonkTime > currentTime - 3000) {
// Bad timing! Show a non-perfect message only if honk was played AFTER this car crossed
// (checking if honk happened within last 3 seconds to ensure it relates to this car)
var nonPerfectMsg = new NonPerfectMessage();
nonPerfectMsg.x = centerX;
nonPerfectMsg.y = centerY;
nonPerfectMsg.creationTime = Date.now(); // Set creation time
game.addChild(nonPerfectMsg);
perfectMessages.push(nonPerfectMsg); // Add to the same array for cleanup
// Explicitly call init to ensure animation starts immediately
nonPerfectMsg.init();
// Normal score increment
LK.setScore(LK.getScore() + 1);
} else {
// Normal score increment
LK.setScore(LK.getScore() + 1);
}
scoreText.setText(LK.getScore().toString());
scoreText.setStyle({
tint: 0xffffff,
stroke: 0xffffff,
strokeThickness: 5
});
// List of available sounds excluding crash
var sounds = ['blind', 'getOff', 'holdingTraffic', 'learnToDrive', 'license', 'moveOver', 'speedUp', 'whatAreYouDoing'];
// Get current last played sound from game variable or initialize it
var lastPlayedSound = game.lastPlayedSound || '';
// Filter out the last played sound if it exists
var availableSounds = sounds.filter(function (sound) {
return sound !== lastPlayedSound;
});
// Select a random sound from the filtered list
var randomSound = availableSounds[Math.floor(Math.random() * availableSounds.length)];
// Store the selected sound as the last played sound
game.lastPlayedSound = randomSound;
// Play the selected sound
LK.getSound(randomSound).play();
// Check if player already has a bubble
if (player.bubble && player.bubble.parent) {
player.bubble.parent.removeChild(player.bubble);
player.bubble.destroy();
}
// Show bubble over player car
var bubble = LK.getAsset('bubble', {
anchorX: 0.5,
anchorY: 1.0,
x: player.x,
y: player.y - player.height / 2
});
game.addChild(bubble);
bubble.scale.set(0);
// Store reference to the bubble in player object
player.bubble = bubble;
// Store creation time for auto-cleanup
bubble.creationTime = Date.now();
// Animate the bubble appearing and disappearing
tween(bubble.scale, {
x: 0.8,
y: 0.8
}, {
duration: 200,
easing: tween.easeOut,
onComplete: function onComplete() {
// Wait for 0.3 seconds then fade out (this happens if no movement removed it)
LK.setTimeout(function () {
// Check if bubble still exists and hasn't been removed by movement
if (bubble && bubble.parent) {
tween(bubble, {
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onComplete: function onComplete() {
if (bubble.parent) {
bubble.parent.removeChild(bubble);
}
bubble.destroy();
// Clear reference in player
if (player.bubble === bubble) {
player.bubble = null;
}
}
});
}
}, 800); // Increased to ensure bubble shows for 1 second total
}
});
}
self.destroy();
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game state variables
var gameStarted = false;
var gameTime = 30; // Initial time in seconds
var distance = 0;
var level = 1;
var checkpointDistance = 500; // Distance between checkpoints
var nextCheckpointAt = checkpointDistance;
var trafficCars = [];
var obstacles = [];
var checkpoints = [];
var timeBonuses = [];
var difficultyTimer;
var lastHonkTime = 0; // Track when player last honked
var perfectMessages = []; // Array to store perfect timing messages
// Create container groups
var palmGroup = new Container();
var roadLinesGroup = new Container();
var SPEED_LEVEL_FACTOR = 3;
// Create road lines
var ROAD_LINE_COUNT = 15;
var roadLines = [];
var BASE_ROAD_LINE_COOLDOWN = 20; // Base spawn cooldown
var roadLineSpawnCooldown = 20; // Spawn a road line every 0.33 seconds if needed
var roadLineSpawnTimer = 0; // Timer to track cooldown
// Create palm trees
var PALM_COUNT = 40;
var palms = [];
var centerX = 2048 / 2; // Center of screen X
var centerY = 2732 / 2; // Center of screen Y
var BASE_PALM_COOLDOWN = 10; // Base spawn cooldown
var palmSpawnCooldown = 10; // Spawn a palm every 0.5 seconds if needed
var palmSpawnTimer = 0; // Timer to track cooldown
var lastSpawnSide = "right"; // Track the last side a palm was spawned on
// Sway parameters
var swayCounter = 0;
// Create road
var road = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2 + 50,
y: 2732 / 2 - 100
// y: 1000
});
game.addChild(road);
game.addChild(palmGroup);
game.addChild(roadLinesGroup);
// Road lines removed as requested
// Create player car
var player = new PlayerCar();
player.x = 2048 / 2;
player.y = 2732 - 300;
game.addChild(player);
var scoreLabel = new Text2('Score:', {
size: 80,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 5
});
scoreLabel.anchor.set(0, 0);
scoreLabel.x = -150;
LK.gui.top.addChild(scoreLabel);
var scoreText = new Text2('0', {
size: 80,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 5
});
scoreText.anchor.set(0, 0);
scoreText.x = 100;
LK.gui.top.addChild(scoreText);
var levelLabel = new Text2('Level:', {
size: 80,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 5
});
levelLabel.anchor.set(0, 0);
levelLabel.x = -150;
levelLabel.y = 100; // Offset below time
LK.gui.top.addChild(levelLabel);
var levelText = new Text2('1', {
size: 80,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 5
});
levelText.anchor.set(0, 0);
levelText.x = 100;
levelText.y = 100; // Offset below time
LK.gui.top.addChild(levelText);
var startText = new Text2('TAP TO START\nTAP TO 🔊\nPERFECTLY TIMED 🔊 = +2', {
size: 120,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 8,
align: 'center'
});
startText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(startText);
// Functions to spawn game elements
function spawnTrafficCar() {
// Only spawn a new car if there are no cars on screen
if (trafficCars.length === 0) {
var car = new TrafficCar();
car.x = 2048 / 2; // Start in the middle of the screen
car.y = car.startY; // Start at the defined startY
car.speed = 0.1 * level * SPEED_LEVEL_FACTOR;
trafficCars.push(car);
game.addChild(car);
}
// Schedule next check for spawning with faster spawns at higher levels
var nextSpawnTime = Math.max(200, 1000 / level); // Faster spawns at higher levels
LK.setTimeout(spawnTrafficCar, nextSpawnTime);
}
// Start game function
function startGame() {
gameStarted = true;
gameTime = 30;
distance = 0;
level = 1; // Initial level starts at 1
nextCheckpointAt = checkpointDistance;
LK.setScore(0);
// Initialize road lines with pre-animated positions as if they had already been running for 10 seconds
roadLines = [];
for (var i = 0; i < roadLinesGroup.children.length; i++) {
roadLinesGroup.children[i].destroy();
}
roadLinesGroup.removeChildren();
roadLineSpawnTimer = 0;
// Create pre-existing road lines as if they were already running for 10 seconds
for (var i = 0; i < ROAD_LINE_COUNT; i++) {
var newRoadLine = new RoadLine();
// Calculate a position along the animation path as if it had been running
// Progress ranges from 0 (just started) to 1 (almost finished)
var progress = i / ROAD_LINE_COUNT;
// Start at center X and interpolate Y based on progress
newRoadLine.x = centerX + 15;
// Calculate Y position - from center-50 to beyond bottom of screen
var startY = centerY - 50;
var targetY = 2732 + 100;
newRoadLine.y = startY + (targetY - startY) * progress;
// Calculate scale based on progress (0.1 to 1.0)
var startScale = 0.1;
var endScale = 1.0;
var currentScale = startScale + (endScale - startScale) * progress;
newRoadLine.scale.set(currentScale);
newRoadLine.lastY = newRoadLine.y;
newRoadLine.isMoving = true;
// Only add the road line and start animation if it's still on screen
if (newRoadLine.y < 2732) {
// Create tween animation with perspective effect - already in progress
var remainingDuration = 4000 * (1 - progress) / level;
tween(newRoadLine, {
y: targetY,
scaleX: endScale,
scaleY: endScale
}, {
duration: remainingDuration,
easing: tween.easeIn
});
roadLines.push(newRoadLine);
roadLinesGroup.addChild(newRoadLine);
} else {
newRoadLine.destroy();
}
}
// Update UI
scoreText.setText('0');
levelText.setText(level.toString());
levelText.setStyle({
tint: 0xffffff,
stroke: 0xffffff,
strokeThickness: 5
});
// Remove start text
LK.gui.center.removeChild(startText);
// Start spawning game elements
spawnTrafficCar();
// Level is now calculated based on score/10+1, no need for a timer to increase it
// Play background music
LK.playMusic('bgmusic');
// Palm spawning is handled in the update function
}
// Handle touch input
game.down = function (x, y) {
// Only play honk sound if game has started
if (!gameStarted) {
startGame();
return;
}
// Play honk sound when screen is clicked/tapped after game has started
LK.getSound('honk').play();
// Record the time of honk for perfect timing detection
lastHonkTime = Date.now();
// Remove any existing bubble when player honks
if (player.bubble && player.bubble.parent) {
player.bubble.parent.removeChild(player.bubble);
player.bubble.destroy();
player.bubble = null;
}
};
game.move = function (x, y) {
if (!gameStarted) {
return;
}
// Move player car directly to mouse X position
var targetX = x;
// Apply smooth movement by calculating the difference
var deltaX = targetX - player.x;
player.steering = deltaX * 0.1; // Smooth movement factor
};
game.up = function () {
if (gameStarted) {
// No need to reset steering as we'll continue to follow mouse
}
};
// Main game update function
game.update = function () {
if (!gameStarted) {
return;
}
// Update road lines
if (gameStarted) {
// Update road line animations
for (var r = roadLines.length - 1; r >= 0; r--) {
var roadLine = roadLines[r];
// Check if roadLine exists before accessing its properties
if (roadLine) {
roadLine.lastY = roadLine.y;
// Start a new roadLine animation if roadLine is not already moving
if (roadLine.isMoving === false) {
roadLine.isMoving = true;
// Set target position - straight down the screen
var targetY = 2732 + 100; // Below bottom of screen
var targetScale = 1.0; // End larger
// Create tween animation with perspective effect
tween(roadLine, {
y: targetY,
scaleX: targetScale,
scaleY: targetScale
}, {
duration: 4000 / level,
easing: tween.easeIn
});
}
// Check if roadLine has moved significantly past the bottom of the screen and destroy it
var destroyYPosition = 2732 - 300; // Check if center is below the bottom edge
if (roadLine.isMoving && roadLine.y > destroyYPosition) {
// RoadLine is off-screen, destroy it
var index = roadLines.indexOf(roadLine);
if (index > -1) {
roadLinesGroup.removeChild(roadLine);
roadLines.splice(index, 1);
roadLine.destroy();
}
// Skip to next roadLine as this one is destroyed
continue;
}
}
} // End of loop processing existing road lines
// Calculate spawn cooldown based on level
roadLineSpawnCooldown = Math.max(5, BASE_ROAD_LINE_COOLDOWN / level);
// Update road line spawn timer and generate new road lines if needed
roadLineSpawnTimer -= 1;
if (roadLineSpawnTimer <= 0 && roadLines.length < ROAD_LINE_COUNT) {
roadLineSpawnTimer = roadLineSpawnCooldown; // Reset timer
var newRoadLine = new RoadLine();
// Start at center X and at center Y
newRoadLine.x = centerX + 15;
newRoadLine.y = centerY - 50;
newRoadLine.scale.set(0.1); // Start small
newRoadLine.lastY = newRoadLine.y;
newRoadLine.isMoving = false; // Flag to track if road line is currently animating
roadLines.push(newRoadLine);
roadLinesGroup.addChild(newRoadLine);
}
}
// Road lines removed as requested
// Update game time
gameTime -= 1 / 60; // Assuming 60 FPS
// Calculate level based on score/10, but minimum of 1
level = Math.floor(LK.getScore() / 10) + 1;
levelText.setText(level.toString());
// Update palm animations section
if (gameStarted) {
distance += 10 + level;
// Don't update score based on distance anymore
// Keep updating the distance for internal calculations if needed
// Update palm animations
for (var p = palms.length - 1; p >= 0; p--) {
var palm = palms[p];
// Check if palm exists before accessing its properties
if (palm) {
palm.lastY = palm.y;
// Start a new palm animation if palm is not already moving
if (palm.isMoving === false) {
palm.isMoving = true;
// Set target position based on direction (left or right corner)
var targetX = palm.direction === "left" ? -12000 : 14500; // Left or right edge
var targetY = 2732 + 100; // Below bottom of screen
var targetScale = 2.0; // End larger
// Create tween animation with perspective effect - use same duration for all palms
tween(palm, {
x: targetX,
y: targetY,
scaleX: targetScale * (lastSpawnSide === "right" ? 1 : -1),
scaleY: targetScale
}, {
duration: 7000 / level,
// Duration decreases as level increases
easing: tween.easeIn // Removed onFinish callback
});
}
// Check if palm has moved significantly past the bottom of the screen and destroy it
var destroyYPosition = 2500; // Check if center is 150px below the bottom edge
if (palm.isMoving && palm.y > destroyYPosition) {
// Palm is off-screen, destroy it
var index = palms.indexOf(palm);
if (index > -1) {
palmGroup.removeChild(palm);
palms.splice(index, 1);
palm.destroy();
}
// Skip to next palm as this one is destroyed
continue;
}
}
} // End of loop processing existing palms
// Calculate palm spawn cooldown based on level
palmSpawnCooldown = Math.max(3, BASE_PALM_COOLDOWN / level);
// Update palm spawn timer and generate new palms if needed
palmSpawnTimer -= 1;
if (palmSpawnTimer <= 0 && palms.length < PALM_COUNT) {
palmSpawnTimer = palmSpawnCooldown; // Reset timer
var newPalm = new Palm();
// Alternate spawn side based on the last spawned side
lastSpawnSide = lastSpawnSide === "right" ? "left" : "right";
// Start from exact center point
newPalm.x = centerX + (lastSpawnSide === "right" ? 50 : -50);
newPalm.y = centerY - 100; // Start at center
newPalm.scale.set(0.005); // Start small
newPalm.scaleX *= lastSpawnSide === "right" ? 1 : -1;
newPalm.lastY = newPalm.y;
newPalm.direction = lastSpawnSide; // Use the determined side
newPalm.isMoving = false; // Flag to track if palm is currently animating
palms.push(newPalm);
palmGroup.addChild(newPalm);
}
}
// Game will continue indefinitely - removed game over condition
gameTime = Math.max(gameTime, 0.1); // Keep time from reaching zero
// Update perfect messages
for (var m = perfectMessages.length - 1; m >= 0; m--) {
perfectMessages[m].update();
}
// Check for old perfect and non-perfect messages every 2 seconds
if (LK.ticks % 60 === 0) {
var currentTime = Date.now();
for (var i = perfectMessages.length - 1; i >= 0; i--) {
// If message is older than 2 seconds and still exists
if (perfectMessages[i] && perfectMessages[i].creationTime && currentTime - perfectMessages[i].creationTime > 500) {
// Remove from game if it's still attached
if (perfectMessages[i].parent) {
perfectMessages[i].parent.removeChild(perfectMessages[i]);
}
// Destroy and remove from array
perfectMessages[i].destroy();
perfectMessages.splice(i, 1);
}
}
// Check for speech bubble timing
if (player.bubble && player.bubble.creationTime && currentTime - player.bubble.creationTime > 1000) {
// Remove bubble if it's been showing for more than 1 second
if (player.bubble.parent) {
player.bubble.parent.removeChild(player.bubble);
}
player.bubble.destroy();
player.bubble = null;
}
}
// Check collisions with traffic cars
for (var i = trafficCars.length - 1; i >= 0; i--) {
var car = trafficCars[i];
if (player.intersects(car) && !player.crashed) {
player.crash();
trafficCars.splice(i, 1);
car.destroy();
// GameOver will be called after the crash sequence
}
}
};
function endGame() {
if (!gameStarted) {
return;
}
// Just save high score without ending the game
if (LK.getScore() > storage.highScore) {
storage.highScore = LK.getScore();
}
// Game continues - no game over screen
}