/**** * 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', 'dog', 'dog', 'dog']; // 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 + 100, y: 2732 / 2 - 230 // 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', 'dog', 'dog', 'dog'];
// 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 + 100,
y: 2732 / 2 - 230
// 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
}