/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Bullet = Container.expand(function () { var self = Container.call(this); var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.speed = -25; self.update = function () { self.y += self.speed; }; return self; }); var Coin = Container.expand(function () { var self = Container.call(this); var coinGraphics = self.attachAsset('coin', { anchorX: 0.5, anchorY: 0.5 }); self.speed = gameSpeed; self.collected = false; self.update = function () { self.y += self.speed; self.rotation += 0.1; }; return self; }); var DebrisParticle = Container.expand(function () { var self = Container.call(this); var debrisGraphics = self.attachAsset('debrisParticle', { anchorX: 0.5, anchorY: 0.5 }); self.speedX = (Math.random() - 0.5) * 20; // Random horizontal speed self.speedY = -5 - Math.random() * 15; // Upward initial speed self.gravity = 0.8; // Gravity acceleration self.lifetime = 90 + Math.random() * 60; // 1.5-2.5 seconds self.maxLifetime = self.lifetime; self.rotationSpeed = (Math.random() - 0.5) * 0.3; // Random colors for debris (metal, glass, etc.) var debrisColors = [0x666666, 0x888888, 0x444444, 0x999999, 0x333333]; debrisGraphics.tint = debrisColors[Math.floor(Math.random() * debrisColors.length)]; self.update = function () { // Apply physics self.x += self.speedX; self.y += self.speedY; self.speedY += self.gravity; // Apply gravity self.rotation += self.rotationSpeed; self.lifetime--; // Fade out over time var fadeProgress = 1 - self.lifetime / self.maxLifetime; self.alpha = Math.max(0, 1 - fadeProgress * 1.5); }; return self; }); var ExhaustParticle = Container.expand(function () { var self = Container.call(this); var particleGraphics = self.attachAsset('exhaustParticle', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 3 + Math.random() * 2; self.sideSpeed = (Math.random() - 0.5) * 2; self.lifetime = 60 + Math.random() * 30; self.maxLifetime = self.lifetime; self.update = function () { self.y += self.speed; self.x += self.sideSpeed; self.lifetime--; // Fade out and scale up over time var fadeProgress = 1 - self.lifetime / self.maxLifetime; self.alpha = Math.max(0, 1 - fadeProgress * 1.5); self.scaleX = 1 + fadeProgress * 0.8; self.scaleY = 1 + fadeProgress * 0.8; }; return self; }); var Flame = Container.expand(function () { var self = Container.call(this); var flameGraphics = self.attachAsset('flame', { anchorX: 0.5, anchorY: 1.0 }); self.speed = -2 - Math.random() * 3; self.sideSpeed = (Math.random() - 0.5) * 4; self.lifetime = 30 + Math.random() * 20; self.maxLifetime = self.lifetime; self.flickerTimer = 0; self.parentCar = null; // Reference to the police car this flame belongs to self.offsetX = 0; // Offset from car position self.offsetY = 0; // Offset from car position self.update = function () { // If attached to a police car, follow its position if (self.parentCar && !self.parentCar.isDestroyed) { self.x = self.parentCar.x + self.offsetX; self.y = self.parentCar.y + self.offsetY; } self.lifetime--; // Flicker effect self.flickerTimer++; if (self.flickerTimer % 3 === 0) { var colors = [0xff4500, 0xff6600, 0xff8800, 0xffaa00]; flameGraphics.tint = colors[Math.floor(Math.random() * colors.length)]; } // Fade and scale over time var fadeProgress = 1 - self.lifetime / self.maxLifetime; self.alpha = Math.max(0, 1 - fadeProgress * 1.2); self.scaleX = 1.5 + fadeProgress * 1.2; // Increased base size from 0.5 to 1.5 self.scaleY = 2.0 + fadeProgress * 2.0; // Increased base size from 0.8 to 2.0 }; return self; }); var Flower = Container.expand(function () { var self = Container.call(this); var flowerGraphics = self.attachAsset('flower', { anchorX: 0.5, anchorY: 1.0 }); self.speed = gameSpeed; self.update = function () { self.y += self.speed; }; return self; }); var HeartPowerUp = Container.expand(function () { var self = Container.call(this); var heartGraphics = self.attachAsset('heart', { anchorX: 0.5, anchorY: 0.5 }); self.speed = gameSpeed; self.collected = false; self.lifetime = 420; // 7 seconds at 60fps self.update = function () { self.y += self.speed; self.rotation += 0.1; // Pulsing effect self.scaleX = 1 + Math.sin(LK.ticks * 0.1) * 0.1; self.scaleY = 1 + Math.sin(LK.ticks * 0.1) * 0.1; // Decrease lifetime self.lifetime--; // Flash when about to disappear (last 3 seconds) if (self.lifetime <= 180 && self.lifetime > 0) { self.alpha = (Math.sin(self.lifetime * 0.3) + 1) * 0.5; } }; return self; }); var LaneLine = Container.expand(function () { var self = Container.call(this); var lineGraphics = self.attachAsset('laneLine', { anchorX: 0.5, anchorY: 0.5 }); // Make lane lines semi-transparent so they appear behind cars lineGraphics.alpha = 0.7; self.speed = gameSpeed; self.update = function () { self.y += self.speed; }; return self; }); var MovingBarrier = Container.expand(function () { var self = Container.call(this); var barrierGraphics = self.attachAsset('movingBarrier', { anchorX: 0.5, anchorY: 1.0 }); self.speed = gameSpeed; self.horizontalSpeed = 2; // Speed of horizontal movement self.direction = 1; // 1 for right, -1 for left self.lanes = [580, 1024, 1468]; // Reference to lane positions self.startLaneIndex = 1; // Start in middle lane by default self.minX = 580; // Will be set based on start lane self.maxX = 1468; // Will be set based on start lane self.changeDirectionTimer = 0; self.changeDirectionDelay = 120 + Math.random() * 180; // 2-5 seconds // Function to set movement boundaries for one lane change only self.setMovementBounds = function (startLaneIndex) { self.startLaneIndex = startLaneIndex; if (startLaneIndex === 0) { // Left lane - can only move to middle lane self.minX = self.lanes[0]; self.maxX = self.lanes[1]; } else if (startLaneIndex === 1) { // Middle lane - can move to left or right lane var canMoveLeft = Math.random() < 0.5; if (canMoveLeft) { self.minX = self.lanes[0]; self.maxX = self.lanes[1]; self.direction = -1; // Start moving left } else { self.minX = self.lanes[1]; self.maxX = self.lanes[2]; self.direction = 1; // Start moving right } } else { // Right lane - can only move to middle lane self.minX = self.lanes[1]; self.maxX = self.lanes[2]; } }; self.update = function () { self.y += self.speed; // Move horizontally self.x += self.horizontalSpeed * self.direction; // Bounce off lane boundaries with smooth animation if (self.x <= self.minX) { self.x = self.minX; self.direction = 1; // Change to move right // Add bounce animation tween(self, { scaleX: 2.6, rotation: 0.1 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 2.4, rotation: 0 }, { duration: 200, easing: tween.easeInOut }); } }); } else if (self.x >= self.maxX) { self.x = self.maxX; self.direction = -1; // Change to move left // Add bounce animation tween(self, { scaleX: 2.6, rotation: -0.1 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 2.4, rotation: 0 }, { duration: 200, easing: tween.easeInOut }); } }); } // Random direction changes for unpredictability self.changeDirectionTimer++; if (self.changeDirectionTimer >= self.changeDirectionDelay) { self.direction *= -1; // Reverse direction self.changeDirectionTimer = 0; self.changeDirectionDelay = 120 + Math.random() * 180; // New random delay // Add direction change animation tween(self, { scaleY: 2.6 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleY: 2.4 }, { duration: 150, easing: tween.easeInOut }); } }); } }; return self; }); var Obstacle = Container.expand(function () { var self = Container.call(this); var obstacleGraphics = self.attachAsset('obstacle', { anchorX: 0.5, anchorY: 1.0 }); self.speed = gameSpeed; // Add police lights to the obstacle (police car) - properly attached to self container // Main roof lights - positioned on top of the police car var leftLight = self.attachAsset('policeLight', { anchorX: 0.5, anchorY: 0.5 }); leftLight.x = -20; leftLight.y = -130; var rightLight = self.attachAsset('policeLight', { anchorX: 0.5, anchorY: 0.5 }); rightLight.x = 20; rightLight.y = -130; rightLight.tint = 0x0000ff; // Make right light blue // Side mirror lights var leftSideLight = self.attachAsset('policeLight', { anchorX: 0.5, anchorY: 0.5 }); leftSideLight.x = -35; leftSideLight.y = -100; var rightSideLight = self.attachAsset('policeLight', { anchorX: 0.5, anchorY: 0.5 }); rightSideLight.x = 35; rightSideLight.y = -100; rightSideLight.tint = 0x0000ff; // Make right side light blue // Front bumper lights var topLeftLight = self.attachAsset('policeLight', { anchorX: 0.5, anchorY: 0.5 }); topLeftLight.x = -25; topLeftLight.y = -145; var topRightLight = self.attachAsset('policeLight', { anchorX: 0.5, anchorY: 0.5 }); topRightLight.x = 25; topRightLight.y = -145; topRightLight.tint = 0x0000ff; // Make top right light blue // Store light references for animation self.leftLight = leftLight; self.rightLight = rightLight; self.leftSideLight = leftSideLight; self.rightSideLight = rightSideLight; self.topLeftLight = topLeftLight; self.topRightLight = topRightLight; self.lightTimer = 0; self.isRedActive = true; // Lane changing properties self.canChangeLanes = false; self.laneChangeTimer = 0; self.laneChangeDelay = 180 + Math.random() * 240; // 3-7 seconds random delay self.targetLaneIndex = Math.floor(Math.random() * 3); // Random target lane self.isChangingLanes = false; self.originalLaneIndex = 0; // Will be set when spawned self.update = function () { self.y += self.speed; // Handle lane changing behavior - only at night if (self.canChangeLanes && !self.isChangingLanes && self.y > 200 && self.y < 2000 && !isDay) { self.laneChangeTimer++; if (self.laneChangeTimer >= self.laneChangeDelay) { // Start lane change self.isChangingLanes = true; var currentLaneIndex = self.originalLaneIndex; // Choose a different lane var availableLanes = []; for (var i = 0; i < 3; i++) { if (i !== currentLaneIndex) { availableLanes.push(i); } } if (availableLanes.length > 0) { var newLaneIndex = availableLanes[Math.floor(Math.random() * availableLanes.length)]; var targetX = player.lanes[newLaneIndex]; // Animate lane change using tween tween(self, { x: targetX }, { duration: 1000 + Math.random() * 1000, easing: tween.easeInOut, onFinish: function onFinish() { self.isChangingLanes = false; self.originalLaneIndex = newLaneIndex; // Set new delay for next potential lane change self.laneChangeTimer = 0; self.laneChangeDelay = 300 + Math.random() * 600; // 5-15 seconds } }); } } } // Police lights visible during day and night, more prominent at night var dayAlpha = isDay ? 0.6 : 1.0; // Visible during day, full at night var dayAlphaLow = isDay ? 0.3 : 0.5; // Reduced for inactive light but still visible // Animate police lights with enhanced tween effects self.lightTimer++; if (self.lightTimer >= 15) { // Change every 15 frames (0.25 seconds at 60fps) self.lightTimer = 0; self.isRedActive = !self.isRedActive; if (self.isRedActive) { // Red lights active - animate with pulse effect tween(self.leftLight, { alpha: dayAlpha, scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut }); tween(self.leftSideLight, { alpha: dayAlpha, scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut }); tween(self.topLeftLight, { alpha: dayAlpha, scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut }); // Blue lights dimmed tween(self.rightLight, { alpha: dayAlphaLow, scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeOut }); tween(self.rightSideLight, { alpha: dayAlphaLow, scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeOut }); tween(self.topRightLight, { alpha: dayAlphaLow, scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeOut }); } else { // Blue lights active - animate with pulse effect tween(self.rightLight, { alpha: dayAlpha, scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut }); tween(self.rightSideLight, { alpha: dayAlpha, scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut }); tween(self.topRightLight, { alpha: dayAlpha, scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut }); // Red lights dimmed tween(self.leftLight, { alpha: dayAlphaLow, scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeOut }); tween(self.leftSideLight, { alpha: dayAlphaLow, scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeOut }); tween(self.topLeftLight, { alpha: dayAlphaLow, scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeOut }); } } }; return self; }); var Player = Container.expand(function () { var self = Container.call(this); var playerGraphics = self.attachAsset('player', { anchorX: 0.5, anchorY: 1.0 }); // Add headlights to the player car - positioned at the front of the vehicle var leftHeadlight = self.attachAsset('headlight', { anchorX: 0.5, anchorY: 0.5 }); leftHeadlight.x = -25; leftHeadlight.y = -150; // Moved further forward to the front of the car leftHeadlight.alpha = 0; // Start invisible var rightHeadlight = self.attachAsset('headlight', { anchorX: 0.5, anchorY: 0.5 }); rightHeadlight.x = 25; rightHeadlight.y = -150; // Moved further forward to the front of the car rightHeadlight.alpha = 0; // Start invisible // Store headlight references self.leftHeadlight = leftHeadlight; self.rightHeadlight = rightHeadlight; // Move headlights to front of all elements self.moveHeadlightsToFront = function () { if (self.parent) { // Remove headlights from current position self.removeChild(leftHeadlight); self.removeChild(rightHeadlight); // Add them back to the front (last children render on top) self.addChild(leftHeadlight); self.addChild(rightHeadlight); } }; self.groundY = 2300; self.lanes = [580, 1024, 1468]; // Three lanes adjusted for wider road self.currentLane = 1; // Middle lane self.targetX = self.lanes[self.currentLane]; self.x = self.targetX; self.y = self.groundY; self.moveLeft = function () { if (self.currentLane > 0) { self.currentLane--; self.targetX = self.lanes[self.currentLane]; // Add shake effect when moving left tween(self, { rotation: -0.1 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { rotation: 0 }, { duration: 100, easing: tween.easeInOut }); } }); // Play car movement sound try { var carMoveSound = LK.getSound('carMove'); carMoveSound.volume = 0.005; carMoveSound.play(); } catch (e) { console.log('Car move sound error:', e); } } }; self.moveRight = function () { if (self.currentLane < 2) { self.currentLane++; self.targetX = self.lanes[self.currentLane]; // Add shake effect when moving right tween(self, { rotation: 0.1 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { rotation: 0 }, { duration: 100, easing: tween.easeInOut }); } }); // Play car movement sound try { var carMoveSound = LK.getSound('carMove'); carMoveSound.volume = 0.005; carMoveSound.play(); } catch (e) { console.log('Car move sound error:', e); } } }; self.update = function () { // Handle lane switching var dx = self.targetX - self.x; if (Math.abs(dx) > 2) { self.x += dx * 0.25; } else { self.x = self.targetX; } // Ensure headlights are always in front self.moveHeadlightsToFront(); }; return self; }); var PoliceChaser = Container.expand(function () { var self = Container.call(this); var policeGraphics = self.attachAsset('policeChaser', { anchorX: 0.5, anchorY: 1.0 }); // Add police lights - same as regular obstacles var leftLight = self.attachAsset('policeLight', { anchorX: 0.5, anchorY: 0.5 }); leftLight.x = -20; leftLight.y = -130; var rightLight = self.attachAsset('policeLight', { anchorX: 0.5, anchorY: 0.5 }); rightLight.x = 20; rightLight.y = -130; rightLight.tint = 0x0000ff; // Store light references for animation self.leftLight = leftLight; self.rightLight = rightLight; self.lightTimer = 0; self.isRedActive = true; // Chase behavior properties self.followDistance = 500; // Distance to maintain behind player self.maxFollowDistance = 700; // Maximum distance to prevent getting too close self.minFollowDistance = 400; // Minimum distance to maintain self.speed = gameSpeed; self.targetX = 1024; // Center lane by default self.lastPlayerLane = 1; // Track player's last lane self.update = function () { // Calculate dynamic follow distance based on current distance to player var currentDistance = self.y - player.y; var targetDistance = self.followDistance; // If too close, increase follow distance if (currentDistance < self.minFollowDistance) { targetDistance = self.maxFollowDistance; } else if (currentDistance > self.maxFollowDistance) { targetDistance = self.minFollowDistance; } // Smoothly adjust position to maintain proper distance var distanceDiff = currentDistance - targetDistance; self.y = player.y + targetDistance + distanceDiff * 0.95; // Gradual adjustment // Follow player's lane with slight delay for realistic behavior var currentPlayerLane = player.currentLane; if (currentPlayerLane !== self.lastPlayerLane) { // Player changed lanes, start following self.targetX = player.lanes[currentPlayerLane]; self.lastPlayerLane = currentPlayerLane; } // Smooth lane following var dx = self.targetX - self.x; if (Math.abs(dx) > 5) { self.x += dx * 0.08; // Slower than player for realistic chase } else { self.x = self.targetX; } // Ensure police lights are always in front self.removeChild(self.leftLight); self.removeChild(self.rightLight); self.addChild(self.leftLight); self.addChild(self.rightLight); // Animate police lights self.lightTimer++; if (self.lightTimer >= 15) { self.lightTimer = 0; self.isRedActive = !self.isRedActive; var dayAlpha = isDay ? 0.8 : 1.0; var dayAlphaLow = isDay ? 0.4 : 0.6; if (self.isRedActive) { // Red light active tween(self.leftLight, { alpha: dayAlpha, scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut }); tween(self.rightLight, { alpha: dayAlphaLow, scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeOut }); } else { // Blue light active tween(self.rightLight, { alpha: dayAlpha, scaleX: 1.2, scaleY: 1.2 }, { duration: 100, easing: tween.easeOut }); tween(self.leftLight, { alpha: dayAlphaLow, scaleX: 1.0, scaleY: 1.0 }, { duration: 100, easing: tween.easeOut }); } } }; return self; }); var PowerUp = Container.expand(function () { var self = Container.call(this); var powerupGraphics = self.attachAsset('powerup', { anchorX: 0.5, anchorY: 0.5 }); self.speed = gameSpeed; self.collected = false; self.lifetime = 420; // 7 seconds at 60fps self.update = function () { self.y += self.speed; self.rotation += 0.15; // Decrease lifetime self.lifetime--; // Flash when about to disappear (last 3 seconds) if (self.lifetime <= 180 && self.lifetime > 0) { self.alpha = (Math.sin(self.lifetime * 0.3) + 1) * 0.5; } }; return self; }); var RainDrop = Container.expand(function () { var self = Container.call(this); var dropGraphics = self.attachAsset('rainDrop', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 15 + Math.random() * 10; // Rain falls fast self.sideSpeed = -2 + Math.random() * 4; // Slight horizontal movement self.alpha = 0.3 + Math.random() * 0.4; // Semi-transparent self.update = function () { self.y += self.speed; self.x += self.sideSpeed; }; return self; }); var Smoke = Container.expand(function () { var self = Container.call(this); var smokeGraphics = self.attachAsset('smoke', { anchorX: 0.5, anchorY: 1.0 }); self.speed = -1 - Math.random() * 2; self.sideSpeed = (Math.random() - 0.5) * 3; self.lifetime = 60 + Math.random() * 40; self.maxLifetime = self.lifetime; self.update = function () { self.y += self.speed; self.x += self.sideSpeed; self.lifetime--; // Fade and expand over time var fadeProgress = 1 - self.lifetime / self.maxLifetime; self.alpha = Math.max(0, 0.8 - fadeProgress * 1.2); self.scaleX = 0.3 + fadeProgress * 2; self.scaleY = 0.3 + fadeProgress * 2; }; return self; }); var Tree = Container.expand(function () { var self = Container.call(this); var treeGraphics = self.attachAsset('tree', { anchorX: 0.5, anchorY: 1.0 }); self.speed = gameSpeed; self.update = function () { self.y += self.speed; }; return self; }); var Tree2 = Container.expand(function () { var self = Container.call(this); var treeGraphics = self.attachAsset('tree2', { anchorX: 0.5, anchorY: 1.0 }); self.speed = gameSpeed; self.update = function () { self.y += self.speed; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x87CEEB }); /**** * Game Code ****/ var player; var obstacles = []; var coins = []; var powerups = []; var heartPowerups = []; var bullets = []; var movingBarriers = []; var hasWeapon = false; var weaponTimer = 0; var weaponDuration = 1200; // 20 seconds at 60fps var heartPowerupSpawnTimer = 0; var movingBarrierSpawnTimer = 0; var laneLines = []; var trees = []; var trees2 = []; var flowers = []; var policeChaser = null; var policeChaserActive = false; var policeChaserTimer = 0; var policeChaserActivationTime = 900; // 15 seconds at 60fps (15 * 60) var policeChaserChaseDuration = 0; // Will be set randomly when activated var policeChaserCooldownDuration = 0; // Will be set randomly when deactivated var policeChaserTimeWindow = 3600; // 1 minute window (60 seconds at 60fps) var policeChaserMaxChaseTime = 600; // 10 seconds maximum chase time per window (10 * 60fps) var policeChaserUsedTime = 0; // Time used in current window var policeChaserWindowTimer = 0; // Timer for the current 1-minute window var gameSpeed = 8; var maxSpeed = 20; var speedIncrement = 0.005; var distance = 0; var obstacleSpawnTimer = 0; var coinSpawnTimer = 0; var powerupSpawnTimer = 0; var laneLineSpawnTimer = 0; var treeSpawnTimer = 0; var lastSwipeX = 0; var lastSwipeY = 0; var swipeStarted = false; var policeMusicPlaying = false; var policeOnScreen = false; var lives = 3; var maxLives = 10; var exhaustParticles = []; var exhaustSpawnTimer = 0; var flames = []; var smokes = []; var debrisParticles = []; var rainDrops = []; var rainSpawnTimer = 0; // Rain cycle variables var isRaining = false; var rainTimer = 0; var rainDuration = 0; // Will be set randomly var rainCooldown = 0; // Will be set randomly var nextRainEvent = 0; // Timer for next rain event var rainSoundPlaying = false; var thunderTimer = 0; var nextThunderTime = 0; // Day/night cycle variables var dayNightTimer = 0; var dayNightCycleDuration = 3600; // 60 seconds (1 minute) at 60fps var isDay = true; var isTransitioning = false; var cycleCount = 0; // Track completed day/night cycles var cycleSpeedBoost = 0; // Additional speed from completed cycles var speedBoostPerCycle = 2; // Speed increase per completed cycle // Game timer for police lane changing feature var gameTimer = 0; var laneChangeStartTime = 7200; // 2 minutes at 60fps (2 * 60 * 60) // Night speed multiplier var nightSpeedMultiplier = 1.25; // 25% faster during night var baseGameSpeed = 8; // Store the base game speed var dayColors = { sky: 0x87CEEB, // Light blue sky forest: 0x228b22, // Forest green road: 0xffffff // Normal road color }; var nightColors = { sky: 0x191970, // Midnight blue forest: 0x0f2f0f, // Dark forest green road: 0x404040 // Darker road color }; // Create road background - widened var roadBackground = game.addChild(LK.getAsset('roadBackground', { anchorX: 0.5, anchorY: 0.5 })); roadBackground.x = 1024; // Center of screen roadBackground.y = 1366; roadBackground.scaleX = 1.3; // Widen road by 30% // Create green forest backgrounds on sides - narrowed var leftForest = game.addChild(LK.getAsset('forestBackground', { anchorX: 0.5, anchorY: 0.5 })); leftForest.x = 190; // Moved closer to center - narrower area leftForest.y = 1366; leftForest.scaleX = 0.8; // Make forest area narrower var rightForest = game.addChild(LK.getAsset('forestBackground', { anchorX: 0.5, anchorY: 0.5 })); rightForest.x = 1858; // Moved closer to center - narrower area rightForest.y = 1366; rightForest.scaleX = 0.8; // Make forest area narrower // Create road borders - adjusted for wider road var leftBorder = game.addChild(LK.getAsset('roadBorder', { anchorX: 0.5, anchorY: 0.5 })); leftBorder.x = 390; // Moved outward for wider road leftBorder.y = 1366; var rightBorder = game.addChild(LK.getAsset('roadBorder', { anchorX: 0.5, anchorY: 0.5 })); rightBorder.x = 1658; // Moved outward for wider road rightBorder.y = 1366; // Spawn initial lane lines to establish proper layering spawnLaneLine(); // Create player player = game.addChild(new Player()); player.scaleX = 2.4; player.scaleY = 2.4; // Create police chaser that follows the player (initially hidden) policeChaser = game.addChild(new PoliceChaser()); policeChaser.scaleX = 2.2; // Slightly smaller than player policeChaser.scaleY = 2.2; policeChaser.x = player.x; // Start in same lane as player policeChaser.visible = false; // Start hidden policeChaser.alpha = 0; // Start transparent // Create score display var scoreText = new Text2('Score: 0', { size: 60, fill: 0xFFFFFF }); scoreText.anchor.set(0.5, 0); LK.gui.top.addChild(scoreText); // Create health bar label var healthLabel = new Text2('Lives:', { size: 40, fill: 0xFFFFFF }); healthLabel.anchor.set(1, 0.5); healthLabel.x = -200; healthLabel.y = 100; LK.gui.topRight.addChild(healthLabel); // Distance display removed - only counting coins for score // Create lives display as hearts in top right with better styling var healthBars = []; for (var h = 0; h < maxLives; h++) { var heart = LK.getAsset('heart', { anchorX: 0.5, anchorY: 0.5 }); // Arrange hearts in two rows for better space usage with 10 hearts var row = Math.floor(h / 5); // 5 hearts per row var col = h % 5; // Column position within row heart.x = -80 - col * 50; // Closer spacing for more hearts heart.y = 80 + row * 50; // Two rows: first at y=80, second at y=130 heart.scaleX = 1.0; // Slightly smaller to fit more hearts heart.scaleY = 1.0; // Add a subtle glow effect heart.alpha = 0.9; // Only show the first 3 hearts initially (player starts with 3 lives) if (h >= 3) { heart.visible = false; heart.alpha = 0; } LK.gui.topRight.addChild(heart); healthBars.push(heart); } // Touch controls game.down = function (x, y, obj) { lastSwipeX = x; lastSwipeY = y; swipeStarted = true; }; game.up = function (x, y, obj) { if (swipeStarted) { var deltaX = x - lastSwipeX; var deltaY = y - lastSwipeY; var swipeThreshold = 100; if (Math.abs(deltaX) > Math.abs(deltaY)) { // Horizontal swipe if (deltaX > swipeThreshold) { player.moveRight(); } else if (deltaX < -swipeThreshold) { player.moveLeft(); } } } swipeStarted = false; }; function spawnObstacle() { var obstacle = new Obstacle(); var laneIndex = Math.floor(Math.random() * 3); obstacle.x = player.lanes[laneIndex]; obstacle.y = -100; obstacle.speed = gameSpeed; obstacle.scaleX = 0.9; obstacle.scaleY = 0.9; // Set original lane index for lane changing obstacle.originalLaneIndex = laneIndex; // Enable lane changing after 2 minutes if game has been running long enough if (gameTimer >= laneChangeStartTime) { obstacle.canChangeLanes = Math.random() < 0.3; // 30% chance to be a lane changer } tween(obstacle, { scaleX: 2.4, scaleY: 2.4 }, { duration: 400, easing: tween.easeOut }); obstacles.push(obstacle); game.addChild(obstacle); } function spawnCoin() { var coin = new Coin(); var laneIndex = Math.floor(Math.random() * 3); var targetX = player.lanes[laneIndex]; var canSpawn = true; // Check if there's an obstacle in this lane that would conflict for (var i = 0; i < obstacles.length; i++) { var obstacle = obstacles[i]; // Check if obstacle is in the same lane and close to spawn area if (Math.abs(obstacle.x - targetX) < 50 && obstacle.y >= -300 && obstacle.y <= 200) { canSpawn = false; break; } } // Check if player is in this lane and close to spawn area if (canSpawn) { if (Math.abs(player.x - targetX) < 100 && player.y >= 2000) { canSpawn = false; } } // Also check if there's a powerup in this lane that would conflict if (canSpawn) { for (var p = 0; p < powerups.length; p++) { var powerup = powerups[p]; // Check if powerup is in the same lane and close to spawn area if (Math.abs(powerup.x - targetX) < 50 && powerup.y >= -300 && powerup.y <= 200) { canSpawn = false; break; } } } // If we can't spawn in the selected lane, try other lanes if (!canSpawn) { var availableLanes = []; for (var lane = 0; lane < 3; lane++) { var laneX = player.lanes[lane]; var laneAvailable = true; // Check for obstacles in this lane for (var j = 0; j < obstacles.length; j++) { var obs = obstacles[j]; if (Math.abs(obs.x - laneX) < 50 && obs.y >= -300 && obs.y <= 200) { laneAvailable = false; break; } } // Check for player in this lane if (laneAvailable) { if (Math.abs(player.x - laneX) < 100 && player.y >= 2000) { laneAvailable = false; } } // Also check for powerups in this lane if (laneAvailable) { for (var k = 0; k < powerups.length; k++) { var pup = powerups[k]; if (Math.abs(pup.x - laneX) < 50 && pup.y >= -300 && pup.y <= 200) { laneAvailable = false; break; } } } if (laneAvailable) { availableLanes.push(lane); } } // If no lanes are available, don't spawn coin this time if (availableLanes.length === 0) { return; } // Pick a random available lane laneIndex = availableLanes[Math.floor(Math.random() * availableLanes.length)]; targetX = player.lanes[laneIndex]; } coin.x = targetX; coin.y = -50; coin.speed = gameSpeed; coin.scaleX = 0.5; coin.scaleY = 0.5; tween(coin, { scaleX: 1, scaleY: 1 }, { duration: 500, easing: tween.easeOut }); coins.push(coin); game.addChild(coin); } function spawnPowerUp() { var powerup = new PowerUp(); var laneIndex = Math.floor(Math.random() * 3); var targetX = player.lanes[laneIndex]; var canSpawn = true; // Check if there's an obstacle in this lane that would conflict for (var i = 0; i < obstacles.length; i++) { var obstacle = obstacles[i]; // Check if obstacle is in the same lane and close to spawn area if (Math.abs(obstacle.x - targetX) < 80 && obstacle.y >= -400 && obstacle.y <= 300) { canSpawn = false; break; } } // If we can't spawn in the selected lane, try other lanes if (!canSpawn) { var availableLanes = []; for (var lane = 0; lane < 3; lane++) { var laneX = player.lanes[lane]; var laneAvailable = true; // Check for obstacles in this lane for (var j = 0; j < obstacles.length; j++) { var obs = obstacles[j]; if (Math.abs(obs.x - laneX) < 80 && obs.y >= -400 && obs.y <= 300) { laneAvailable = false; break; } } if (laneAvailable) { availableLanes.push(lane); } } // If no lanes are available, don't spawn powerup this time if (availableLanes.length === 0) { return; } // Pick a random available lane laneIndex = availableLanes[Math.floor(Math.random() * availableLanes.length)]; targetX = player.lanes[laneIndex]; } powerup.x = targetX; powerup.y = -50; powerup.speed = gameSpeed; powerup.scaleX = 0.3; powerup.scaleY = 0.3; tween(powerup, { scaleX: 1, scaleY: 1 }, { duration: 600, easing: tween.easeOut }); powerups.push(powerup); game.addChild(powerup); } function spawnLaneLine() { // Lane dividers between lanes - adjusted for wider road var positions = [802, 1246]; // Between lanes 0-1 and 1-2, proportionally spaced for (var i = 0; i < positions.length; i++) { var laneLine = new LaneLine(); laneLine.x = positions[i]; laneLine.y = -20; laneLine.speed = gameSpeed; laneLines.push(laneLine); // Simply add lane lines normally - layering optimization removed for performance game.addChild(laneLine); } } function spawnExhaustParticle() { var particle = new ExhaustParticle(); particle.x = player.x + (Math.random() - 0.5) * 40; particle.y = player.y + 20; particle.scaleX = 0.5 + Math.random() * 0.5; particle.scaleY = 0.5 + Math.random() * 0.5; particle.alpha = 0.6 + Math.random() * 0.4; exhaustParticles.push(particle); game.addChild(particle); } function createFlameEffect(x, y, parentCar) { // Create multiple flame particles (optimized amount) for (var i = 0; i < 8; i++) { // Balanced flame count for performance var flame = new Flame(); flame.x = x + (Math.random() - 0.5) * 80; // Increased spread from 60 to 80 flame.y = y + Math.random() * 40; // Increased spread from 30 to 40 flame.scaleX = 1.2 + Math.random() * 0.8; // Increased base size from 0.5 to 1.2 flame.scaleY = 1.2 + Math.random() * 0.8; // Increased base size from 0.5 to 1.2 // Attach flame to police car if provided if (parentCar) { flame.parentCar = parentCar; flame.offsetX = (Math.random() - 0.5) * 80; flame.offsetY = Math.random() * 40 - 60; // Flames above the car } flames.push(flame); game.addChild(flame); } // Create smoke particles (optimized amount) for (var j = 0; j < 4; j++) { // Balanced smoke particle count var smoke = new Smoke(); smoke.x = x + (Math.random() - 0.5) * 50; // Increased spread from 40 to 50 smoke.y = y - 10 + Math.random() * 30; // Increased spread from 20 to 30 smoke.alpha = 0.4 + Math.random() * 0.3; smoke.scaleX = 1.5 + Math.random() * 0.5; // Increased smoke size smoke.scaleY = 1.5 + Math.random() * 0.5; // Increased smoke size smokes.push(smoke); game.addChild(smoke); } // Create debris particles for explosion effect (optimized amount) for (var k = 0; k < 10; k++) { var debris = new DebrisParticle(); debris.x = x + (Math.random() - 0.5) * 40; debris.y = y + (Math.random() - 0.5) * 30; debris.scaleX = 0.5 + Math.random() * 1.0; debris.scaleY = 0.5 + Math.random() * 1.0; debrisParticles.push(debris); game.addChild(debris); } } function spawnTrees() { // Spawn trees on left side of road - adjusted for narrower forest var leftPositions = [120, 200, 280, 340]; for (var i = 0; i < leftPositions.length; i++) { // Randomly choose between Tree and Tree2 if (Math.random() < 0.65) { var leftTree; if (Math.random() < 0.5) { leftTree = new Tree(); trees.push(leftTree); } else { leftTree = new Tree2(); trees2.push(leftTree); } leftTree.x = leftPositions[i]; leftTree.y = -80; leftTree.speed = gameSpeed; // Add some variation in scale - bigger trees leftTree.scaleX = 1.2 + Math.random() * 0.8; leftTree.scaleY = 1.2 + Math.random() * 0.8; // Add slight tint variation for natural look var tintVariation = 0.9 + Math.random() * 0.2; leftTree.tint = tintVariation * 0x90EE90 | 0; // Apply current day/night brightness if (!isDay) { leftTree.alpha = 0.6; } // Simply add trees normally - layering optimization removed for performance game.addChild(leftTree); } // Spawn flowers in left forest area if (Math.random() < 0.25) { var leftFlower = new Flower(); leftFlower.x = leftPositions[i] + (Math.random() - 0.5) * 80; leftFlower.y = -40; leftFlower.speed = gameSpeed; leftFlower.scaleX = 0.8 + Math.random() * 0.4; leftFlower.scaleY = 0.8 + Math.random() * 0.4; // Add flower color variations var flowerColors = [0xff69b4, 0xff1493, 0xffffff, 0xffff00, 0xff4500]; leftFlower.tint = flowerColors[Math.floor(Math.random() * flowerColors.length)]; if (!isDay) { leftFlower.alpha = 0.5; } flowers.push(leftFlower); game.addChild(leftFlower); } } // Spawn trees on right side of road - adjusted for narrower forest var rightPositions = [1708, 1788, 1868, 1928]; for (var j = 0; j < rightPositions.length; j++) { // Randomly choose between Tree and Tree2 if (Math.random() < 0.65) { var rightTree; if (Math.random() < 0.5) { rightTree = new Tree(); trees.push(rightTree); } else { rightTree = new Tree2(); trees2.push(rightTree); } rightTree.x = rightPositions[j]; rightTree.y = -80; rightTree.speed = gameSpeed; // Add some variation in scale - bigger trees rightTree.scaleX = 1.2 + Math.random() * 0.8; rightTree.scaleY = 1.2 + Math.random() * 0.8; // Add slight tint variation for natural look var tintVariation = 0.9 + Math.random() * 0.2; rightTree.tint = tintVariation * 0x90EE90 | 0; // Apply current day/night brightness if (!isDay) { rightTree.alpha = 0.6; } // Simply add trees normally - layering optimization removed for performance game.addChild(rightTree); } // Spawn flowers in right forest area if (Math.random() < 0.25) { var rightFlower = new Flower(); rightFlower.x = rightPositions[j] + (Math.random() - 0.5) * 80; rightFlower.y = -40; rightFlower.speed = gameSpeed; rightFlower.scaleX = 0.8 + Math.random() * 0.4; rightFlower.scaleY = 0.8 + Math.random() * 0.4; // Add flower color variations var flowerColors = [0xff69b4, 0xff1493, 0xffffff, 0xffff00, 0xff4500]; rightFlower.tint = flowerColors[Math.floor(Math.random() * flowerColors.length)]; if (!isDay) { rightFlower.alpha = 0.5; } flowers.push(rightFlower); game.addChild(rightFlower); } } } function spawnHeartPowerUp() { var heartPowerup = new HeartPowerUp(); var laneIndex = Math.floor(Math.random() * 3); var targetX = player.lanes[laneIndex]; var canSpawn = true; // Check if there's an obstacle in this lane that would conflict for (var i = 0; i < obstacles.length; i++) { var obstacle = obstacles[i]; // Check if obstacle is in the same lane and close to spawn area if (Math.abs(obstacle.x - targetX) < 80 && obstacle.y >= -400 && obstacle.y <= 300) { canSpawn = false; break; } } // If we can't spawn in the selected lane, try other lanes if (!canSpawn) { var availableLanes = []; for (var lane = 0; lane < 3; lane++) { var laneX = player.lanes[lane]; var laneAvailable = true; // Check for obstacles in this lane for (var j = 0; j < obstacles.length; j++) { var obs = obstacles[j]; if (Math.abs(obs.x - laneX) < 80 && obs.y >= -400 && obs.y <= 300) { laneAvailable = false; break; } } if (laneAvailable) { availableLanes.push(lane); } } // If no lanes are available, don't spawn heart powerup this time if (availableLanes.length === 0) { return; } // Pick a random available lane laneIndex = availableLanes[Math.floor(Math.random() * availableLanes.length)]; targetX = player.lanes[laneIndex]; } heartPowerup.x = targetX; heartPowerup.y = -50; heartPowerup.speed = gameSpeed; heartPowerup.scaleX = 0.3; heartPowerup.scaleY = 0.3; tween(heartPowerup, { scaleX: 1, scaleY: 1 }, { duration: 600, easing: tween.easeOut }); heartPowerups.push(heartPowerup); game.addChild(heartPowerup); } function spawnMovingBarrier() { var barrier = new MovingBarrier(); // Choose random starting lane var startLaneIndex = Math.floor(Math.random() * 3); barrier.x = player.lanes[startLaneIndex]; barrier.y = -100; barrier.speed = gameSpeed; barrier.scaleX = 0.9; barrier.scaleY = 0.9; // Set movement bounds for one lane change only barrier.setMovementBounds(startLaneIndex); // Animate barrier scaling up when spawned tween(barrier, { scaleX: 2.4, scaleY: 2.4 }, { duration: 400, easing: tween.easeOut }); movingBarriers.push(barrier); game.addChild(barrier); } function spawnRain() { // Spawn multiple rain drops across the screen width (optimized amount) for (var i = 0; i < 6; i++) { var rainDrop = new RainDrop(); rainDrop.x = Math.random() * 2048; // Random X across screen width rainDrop.y = -20 - Math.random() * 50; // Start above screen rainDrop.speed = gameSpeed + 15 + Math.random() * 10; // Rain speed based on game speed rainDrops.push(rainDrop); game.addChild(rainDrop); } } // Main game loop game.update = function () { // Update game timer gameTimer++; // Handle police chaser timing and visibility with 1-minute window limit policeChaserTimer++; policeChaserWindowTimer++; // Reset the time window every minute if (policeChaserWindowTimer >= policeChaserTimeWindow) { policeChaserWindowTimer = 0; policeChaserUsedTime = 0; // Reset used time for new window console.log('Police chaser time window reset - new 1 minute period'); } if (!policeChaserActive) { // Check if it's time to activate police chaser and if we have time left in current window if (policeChaserTimer >= policeChaserActivationTime && policeChaserUsedTime < policeChaserMaxChaseTime) { // Calculate remaining time in current window var remainingTime = policeChaserMaxChaseTime - policeChaserUsedTime; // Activate police chaser policeChaserActive = true; policeChaserTimer = 0; // Set chase duration to minimum of remaining time or desired duration (max 10 seconds) policeChaserChaseDuration = Math.min(remainingTime, 600); // Max 10 seconds or remaining time // Position police chaser off-screen behind player and make visible policeChaser.visible = true; policeChaser.alpha = 0; // Start transparent for smooth fade in policeChaser.y = player.y + 1200; // Start well behind player off-screen policeChaser.x = player.x; // Start in same lane as player // Fade in police chaser first tween(policeChaser, { alpha: 1 }, { duration: 1500, easing: tween.easeInOut }); // Smoothly move police chaser into proper following position tween(policeChaser, { y: player.y + policeChaser.followDistance }, { duration: 3000, // Slower entry for smoother appearance easing: tween.easeInOut }); console.log('Police chaser activated for', Math.floor(policeChaserChaseDuration / 60), 'seconds. Used time:', Math.floor(policeChaserUsedTime / 60), 'Remaining:', Math.floor(remainingTime / 60)); } } else { // Police chaser is active, track used time and check if chase duration is over policeChaserUsedTime++; // Track time being used if (policeChaserTimer >= policeChaserChaseDuration || policeChaserUsedTime >= policeChaserMaxChaseTime) { // Deactivate police chaser policeChaserActive = false; policeChaserTimer = 0; // Set cooldown duration - wait until next window if we've used all time if (policeChaserUsedTime >= policeChaserMaxChaseTime) { // Used all time in window, wait until window resets var timeUntilWindowReset = policeChaserTimeWindow - policeChaserWindowTimer; policeChaserActivationTime = timeUntilWindowReset + 300; // Wait for window reset + 5 seconds console.log('Police chaser time limit reached. Waiting', Math.floor(timeUntilWindowReset / 60), 'seconds for window reset'); } else { // Still have time left, set normal cooldown policeChaserActivationTime = 1800 + Math.random() * 1800; // 30-60 seconds } // Make police chaser fall behind by reducing its speed and moving it back tween(policeChaser, { y: policeChaser.y + 1000 // Move it further behind the player }, { duration: 4000, // Slower transition over 4 seconds easing: tween.easeInOut, onFinish: function onFinish() { // After falling behind, fade out and hide tween(policeChaser, { alpha: 0 }, { duration: 1500, // Longer fade out easing: tween.easeInOut, onFinish: function onFinish() { policeChaser.visible = false; // Reset position completely off-screen to prevent any issues policeChaser.y = player.y + 1500; } }); } }); console.log('Police chaser deactivated. Next activation in', Math.floor(policeChaserActivationTime / 60), 'seconds'); } } // Calculate current base speed with gradual increase and cycle boost var currentBaseSpeed = Math.min(maxSpeed, baseGameSpeed + gameTimer * speedIncrement + cycleSpeedBoost); // Apply night speed multiplier gameSpeed = isDay ? currentBaseSpeed : currentBaseSpeed * nightSpeedMultiplier; // Distance tracking removed - only counting coins for score // Spawn obstacles - optimized spawn rates for better gameplay flow obstacleSpawnTimer++; var obstacleSpawnRate = isDay ? 150 + Math.random() * 60 : 120 + Math.random() * 90; // More balanced spawn rates if (obstacleSpawnTimer > obstacleSpawnRate) { spawnObstacle(); obstacleSpawnTimer = 0; } // Spawn coins - optimized spawn rate for better balance coinSpawnTimer++; var coinSpawnRate = Math.max(40, 100 - (gameSpeed - 8) * 3); // Balanced coin spawning if (coinSpawnTimer > coinSpawnRate + Math.random() * 30) { spawnCoin(); coinSpawnTimer = 0; } // Spawn powerups (rare spawn - every 1 minute) powerupSpawnTimer++; if (powerupSpawnTimer > 3600) { // 1 minute (60 seconds * 60 fps) spawnPowerUp(); powerupSpawnTimer = 0; } // Spawn heart powerups (every 3 minutes) heartPowerupSpawnTimer++; if (heartPowerupSpawnTimer > 10800) { // 3 minutes (180 seconds * 60 fps) spawnHeartPowerUp(); heartPowerupSpawnTimer = 0; } // Spawn moving barriers (every 45 seconds) movingBarrierSpawnTimer++; if (movingBarrierSpawnTimer > 2700) { // 45 seconds (45 * 60 fps) spawnMovingBarrier(); movingBarrierSpawnTimer = 0; } // Spawn lane lines laneLineSpawnTimer++; if (laneLineSpawnTimer > 30) { spawnLaneLine(); laneLineSpawnTimer = 0; } // Spawn trees treeSpawnTimer++; if (treeSpawnTimer > 45) { spawnTrees(); treeSpawnTimer = 0; } // Update obstacles var currentPoliceOnScreen = false; for (var i = obstacles.length - 1; i >= 0; i--) { var obstacle = obstacles[i]; obstacle.speed = gameSpeed; // Check if police car is on screen if (obstacle.y >= -200 && obstacle.y <= 2900) { currentPoliceOnScreen = true; } // Initialize fade tracking if needed if (obstacle.lastY === undefined) obstacle.lastY = obstacle.y; if (obstacle.isFadingOut === undefined) obstacle.isFadingOut = false; // Check if obstacle just crossed the fade threshold if (!obstacle.isFadingOut && obstacle.lastY <= 2800 && obstacle.y > 2800) { obstacle.isFadingOut = true; // Start fade out animation tween(obstacle, { alpha: 0, scaleX: obstacle.scaleX * 0.7, scaleY: obstacle.scaleY * 0.7 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { obstacle.destroy(); // Remove from obstacles array after fade completes var index = obstacles.indexOf(obstacle); if (index > -1) { obstacles.splice(index, 1); } } }); } // Update last position obstacle.lastY = obstacle.y; // Skip further processing if already fading or destroyed if (obstacle.isFadingOut || obstacle.y > 3500) { continue; } // Check collision with player (skip if obstacle is already hit) if (!obstacle.isHit && player.intersects(obstacle)) { // Play crash sound try { LK.getSound('crash').play(); } catch (e) { console.log('Crash sound error:', e); } // Reduce lives lives--; // Update health bar display - hide the lost heart if (lives >= 0 && lives < maxLives) { var lostHeart = healthBars[lives]; // Use current lives as index (lives already decremented) if (lostHeart) { // Animate heart disappearing with red flash tween(lostHeart, { alpha: 0, scaleX: 0.5, scaleY: 0.5, tint: 0xff0000 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { lostHeart.visible = false; } }); } } // Flash screen and remove obstacle LK.effects.flashScreen(0xff0000, 500); // Mark obstacle as hit to prevent multiple collisions obstacle.isHit = true; // Check if game over if (lives <= 0) { // Add delay to allow crash sound to play before game over LK.setTimeout(function () { LK.showGameOver(); }, 800); // Wait 800ms for crash sound to play return; } // Make player temporarily invulnerable player.alpha = 0.5; LK.setTimeout(function () { player.alpha = 1.0; }, 1500); // 1.5 seconds of invulnerability continue; } } // Handle police music based on police presence if (currentPoliceOnScreen && !policeMusicPlaying) { // Start police siren as a sound effect that loops instead of music // This allows it to play alongside the background music try { var policeSiren = LK.getSound('policemusic'); if (policeSiren) { policeSiren.loop = true; policeSiren.play(); } } catch (e) { console.log('Police siren play error:', e); } policeMusicPlaying = true; policeOnScreen = true; } else if (!currentPoliceOnScreen && policeMusicPlaying) { // Stop police siren sound try { var policeSiren = LK.getSound('policemusic'); if (policeSiren) { policeSiren.stop(); } } catch (e) { console.log('Police siren stop error:', e); } policeMusicPlaying = false; policeOnScreen = false; } else if (currentPoliceOnScreen && policeMusicPlaying) { // Ensure police siren continues playing if it stopped try { var policeSiren = LK.getSound('policemusic'); if (policeSiren && !policeSiren.playing) { policeSiren.loop = true; policeSiren.play(); } } catch (e) { console.log('Police siren restart error:', e); } } // Update coins for (var j = coins.length - 1; j >= 0; j--) { var coin = coins[j]; coin.speed = gameSpeed; // Remove coins that are off screen if (coin.y > 2800) { coin.destroy(); coins.splice(j, 1); continue; } // Check collection if (!coin.collected && player.intersects(coin)) { coin.collected = true; LK.setScore(LK.getScore() + 1); // Each coin is worth exactly 1 point scoreText.setText('Score: ' + LK.getScore()); LK.getSound('collect').play(); // Enhanced visual effect with faster animation for higher speeds var animationDuration = Math.max(200, 400 - (gameSpeed - 8) * 10); tween(coin, { alpha: 0, scaleX: 2.5, scaleY: 2.5, y: coin.y - 50 }, { duration: animationDuration, easing: tween.easeOut, onFinish: function onFinish() { coin.destroy(); } }); coins.splice(j, 1); } } // Update powerups for (var p = powerups.length - 1; p >= 0; p--) { var powerup = powerups[p]; powerup.speed = gameSpeed; // Remove powerups that are off screen or expired if (powerup.y > 2800 || powerup.lifetime <= 0) { powerup.destroy(); powerups.splice(p, 1); continue; } // Check collection if (!powerup.collected && player.intersects(powerup)) { powerup.collected = true; hasWeapon = true; weaponTimer = weaponDuration; // 20 seconds LK.getSound('collect').play(); // Visual effect tween(powerup, { alpha: 0, scaleX: 3, scaleY: 3 }, { duration: 400, onFinish: function onFinish() { powerup.destroy(); } }); powerups.splice(p, 1); } } // Update heart powerups for (var hp = heartPowerups.length - 1; hp >= 0; hp--) { var heartPowerup = heartPowerups[hp]; heartPowerup.speed = gameSpeed; // Remove heart powerups that are off screen or expired if (heartPowerup.y > 2800 || heartPowerup.lifetime <= 0) { heartPowerup.destroy(); heartPowerups.splice(hp, 1); continue; } // Check collection if (!heartPowerup.collected && player.intersects(heartPowerup)) { heartPowerup.collected = true; // Add one life if not at maximum if (lives < maxLives) { lives++; // Show the health bar corresponding to gained life with animation if (lives > 0 && lives <= maxLives) { var restoredHeart = healthBars[lives - 1]; // Use lives-1 as index (lives already incremented) if (restoredHeart) { restoredHeart.visible = true; restoredHeart.alpha = 0; restoredHeart.scaleX = 2.0; restoredHeart.scaleY = 2.0; restoredHeart.tint = 0x00ff00; // Green flash // Animate heart appearing tween(restoredHeart, { alpha: 0.9, scaleX: 1.0, scaleY: 1.0, tint: 0xffffff }, { duration: 600, easing: tween.easeOut }); } } } LK.getSound('collect').play(); // Visual effect with heart animation tween(heartPowerup, { alpha: 0, scaleX: 2.5, scaleY: 2.5, y: heartPowerup.y - 100 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { heartPowerup.destroy(); } }); heartPowerups.splice(hp, 1); } } // Update lane lines for (var k = laneLines.length - 1; k >= 0; k--) { var laneLine = laneLines[k]; laneLine.speed = gameSpeed; // Remove lane lines that are off screen if (laneLine.y > 2800) { laneLine.destroy(); laneLines.splice(k, 1); } } // Update trees for (var t = trees.length - 1; t >= 0; t--) { var tree = trees[t]; tree.speed = gameSpeed; // Remove trees that are off screen - increased threshold to prevent immediate disappearing if (tree.y > 3200) { tree.destroy(); trees.splice(t, 1); } } // Update trees2 for (var t2 = trees2.length - 1; t2 >= 0; t2--) { var tree2 = trees2[t2]; tree2.speed = gameSpeed; // Remove trees2 that are off screen - increased threshold to prevent immediate disappearing if (tree2.y > 3200) { tree2.destroy(); trees2.splice(t2, 1); } } // Update flowers for (var fl = flowers.length - 1; fl >= 0; fl--) { var flower = flowers[fl]; flower.speed = gameSpeed; // Remove flowers that are off screen if (flower.y > 3200) { flower.destroy(); flowers.splice(fl, 1); } } // Handle rain cycle logic if (!isRaining) { // Check if it's time to start raining nextRainEvent++; if (nextRainEvent >= rainCooldown) { // Start rain cycle isRaining = true; rainTimer = 0; // Random rain duration: 10-60 seconds (600-3600 frames at 60fps) rainDuration = 600 + Math.random() * 3000; // 10 seconds to 1 minute nextRainEvent = 0; // Start rain music if (!rainSoundPlaying) { try { LK.playMusic('rain'); } catch (e) { console.log('Rain music play error:', e); } rainSoundPlaying = true; } // Initialize thunder timing for this rain period thunderTimer = 0; nextThunderTime = 180 + Math.random() * 600; // First thunder in 3-13 seconds console.log('Rain started for', Math.floor(rainDuration / 60), 'seconds'); } } else { // Currently raining rainTimer++; // Spawn rain particles while raining - optimized frequency for performance rainSpawnTimer++; if (rainSpawnTimer >= 6) { // Spawn rain every 6 frames for balanced performance spawnRain(); rainSpawnTimer = 0; } // Handle thunder during rain thunderTimer++; if (thunderTimer >= nextThunderTime) { // Play thunder sound try { var thunderSound = LK.getSound('thunder'); if (thunderSound) { thunderSound.play(); } } catch (e) { console.log('Thunder sound play error:', e); } // Add thunder flash effect - bright white flash LK.effects.flashScreen(0xffffff, 300); // White flash for 300ms // Set next thunder time (random between 5-20 seconds) nextThunderTime = thunderTimer + (300 + Math.random() * 900); // 5-20 seconds at 60fps } // Check if rain should stop if (rainTimer >= rainDuration) { // Stop rain cycle isRaining = false; rainTimer = 0; // Random cooldown period: 30-180 seconds (1800-10800 frames at 60fps) rainCooldown = 1800 + Math.random() * 9000; // 30 seconds to 3 minutes nextRainEvent = 0; // Reset thunder timer thunderTimer = 0; nextThunderTime = 0; // Gradually fade out existing rain drops for (var fadeRain = 0; fadeRain < rainDrops.length; fadeRain++) { var rainDrop = rainDrops[fadeRain]; tween(rainDrop, { alpha: 0, speed: rainDrop.speed * 0.3 }, { duration: 2000 + Math.random() * 1000, easing: tween.easeOut }); } console.log('Rain stopped. Next rain in', Math.floor(rainCooldown / 60), 'seconds'); } } // Stop rain music only when all rain effects are complete if (!isRaining && rainSoundPlaying && rainDrops.length === 0) { try { LK.stopMusic(); // Restart background music after rain stops LK.playMusic('bgmusic'); } catch (e) { console.log('Rain music stop error:', e); } rainSoundPlaying = false; } // Spawn exhaust particles - optimized frequency for performance exhaustSpawnTimer++; if (exhaustSpawnTimer >= 12) { spawnExhaustParticle(); exhaustSpawnTimer = 0; } // Update exhaust particles for (var e = exhaustParticles.length - 1; e >= 0; e--) { var particle = exhaustParticles[e]; // Remove particles that are expired or off screen if (particle.lifetime <= 0 || particle.y < -50) { particle.destroy(); exhaustParticles.splice(e, 1); } } // Update flame particles for (var f = flames.length - 1; f >= 0; f--) { var flame = flames[f]; // Remove flames that are expired or off screen if (flame.lifetime <= 0 || flame.y < -50) { flame.destroy(); flames.splice(f, 1); } } // Update smoke particles for (var s = smokes.length - 1; s >= 0; s--) { var smoke = smokes[s]; // Remove smoke that is expired or off screen if (smoke.lifetime <= 0 || smoke.y < -50) { smoke.destroy(); smokes.splice(s, 1); } } // Update debris particles for (var d = debrisParticles.length - 1; d >= 0; d--) { var debris = debrisParticles[d]; // Remove debris that is expired or off screen if (debris.lifetime <= 0 || debris.y > 2800) { debris.destroy(); debrisParticles.splice(d, 1); } } // Update rain particles for (var r = rainDrops.length - 1; r >= 0; r--) { var rain = rainDrops[r]; // Remove rain drops that are off screen if (rain.y > 2800 || rain.x < -50 || rain.x > 2098) { rain.destroy(); rainDrops.splice(r, 1); } } // Update moving barriers for (var mb = movingBarriers.length - 1; mb >= 0; mb--) { var barrier = movingBarriers[mb]; barrier.speed = gameSpeed; // Initialize fade tracking if needed if (barrier.lastY === undefined) barrier.lastY = barrier.y; if (barrier.isFadingOut === undefined) barrier.isFadingOut = false; // Check if barrier just crossed the fade threshold if (!barrier.isFadingOut && barrier.lastY <= 2800 && barrier.y > 2800) { barrier.isFadingOut = true; // Start fade out animation tween(barrier, { alpha: 0, scaleX: barrier.scaleX * 0.7, scaleY: barrier.scaleY * 0.7 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { barrier.destroy(); // Remove from movingBarriers array after fade completes var index = movingBarriers.indexOf(barrier); if (index > -1) { movingBarriers.splice(index, 1); } } }); } // Update last position barrier.lastY = barrier.y; // Skip further processing if already fading or destroyed if (barrier.isFadingOut || barrier.y > 3500) { continue; } // Check collision with player (skip if barrier is already hit) if (!barrier.isHit && player.intersects(barrier)) { // Play crash sound try { LK.getSound('crash').play(); } catch (e) { console.log('Crash sound error:', e); } // Reduce lives lives--; // Update health bar display - hide the lost heart if (lives >= 0 && lives < maxLives) { var lostHeart = healthBars[lives]; // Use current lives as index (lives already decremented) if (lostHeart) { // Animate heart disappearing with red flash tween(lostHeart, { alpha: 0, scaleX: 0.5, scaleY: 0.5, tint: 0xff0000 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { lostHeart.visible = false; } }); } } // Flash screen and mark barrier as hit LK.effects.flashScreen(0xff0000, 500); barrier.isHit = true; // Check if game over if (lives <= 0) { LK.setTimeout(function () { LK.showGameOver(); }, 800); return; } // Make player temporarily invulnerable player.alpha = 0.5; LK.setTimeout(function () { player.alpha = 1.0; }, 1500); continue; } // Check bullet collision with moving barriers for (var bb = bullets.length - 1; bb >= 0; bb--) { var bullet = bullets[bb]; if (bullet && !barrier.isHit && bullet.intersects(barrier)) { // Play police hit sound try { LK.getSound('policeHit').play(); } catch (e) { console.log('Police hit sound error:', e); } // Destroy bullet immediately bullet.destroy(); bullets.splice(bb, 1); // Mark barrier as hit barrier.isHit = true; // Create flame effect at barrier position createFlameEffect(barrier.x, barrier.y - 50, barrier); // Create gold coins flying out when barrier is destroyed (optimized amount) for (var goldCount = 0; goldCount < 5; goldCount++) { var goldCoin = new Coin(); goldCoin.x = barrier.x + (Math.random() - 0.5) * 100; goldCoin.y = barrier.y + (Math.random() - 0.5) * 80; goldCoin.scaleX = 0.8 + Math.random() * 0.4; goldCoin.scaleY = 0.8 + Math.random() * 0.4; goldCoin.collected = false; goldCoin.speed = gameSpeed * 0.5; goldCoin.tint = 0xFFD700; var randomX = goldCoin.x + (Math.random() - 0.5) * 200; var randomY = goldCoin.y + Math.random() * 150 + 50; tween(goldCoin, { x: randomX, y: randomY, rotation: Math.PI * 4 }, { duration: 800 + Math.random() * 400, easing: tween.easeOut }); coins.push(goldCoin); game.addChild(goldCoin); } // Create burning effect tween(barrier, { tint: 0x000000, alpha: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { barrier.isDestroyed = true; barrier.destroy(); var index = movingBarriers.indexOf(barrier); if (index > -1) { movingBarriers.splice(index, 1); } } }); break; } } } // Handle weapon system if (hasWeapon) { weaponTimer--; if (weaponTimer <= 0) { hasWeapon = false; } // Shoot bullets every 15 frames for balanced firing rate if (LK.ticks % 15 === 0) { var bullet = new Bullet(); bullet.x = player.x; bullet.y = player.y - 100; bullets.push(bullet); game.addChild(bullet); // Play gun sound with reduced volume for less audio clutter try { var gunSound = LK.getSound('gunshot'); gunSound.volume = 0.2; gunSound.play(); } catch (e) { console.log('Gun sound error:', e); } } } // Update bullets for (var b = bullets.length - 1; b >= 0; b--) { var bullet = bullets[b]; // Remove bullets that are off screen if (bullet.y < -50) { bullet.destroy(); bullets.splice(b, 1); continue; } // Check bullet collision with obstacles for (var o = obstacles.length - 1; o >= 0; o--) { var obstacle = obstacles[o]; if (!obstacle.isHit && bullet.intersects(obstacle)) { // Play police hit sound try { LK.getSound('policeHit').play(); } catch (e) { console.log('Police hit sound error:', e); } // Destroy bullet immediately bullet.destroy(); bullets.splice(b, 1); // Mark obstacle as hit to prevent further collisions obstacle.isHit = true; // Create flame effect at police car position with car reference createFlameEffect(obstacle.x, obstacle.y - 50, obstacle); // Create 6 gold coins flying out when police car is destroyed (optimized amount) for (var goldCount = 0; goldCount < 6; goldCount++) { var goldCoin = new Coin(); goldCoin.x = obstacle.x + (Math.random() - 0.5) * 100; goldCoin.y = obstacle.y + (Math.random() - 0.5) * 80; goldCoin.scaleX = 0.8 + Math.random() * 0.4; goldCoin.scaleY = 0.8 + Math.random() * 0.4; goldCoin.collected = false; goldCoin.speed = gameSpeed * 0.5; // Slower than normal coins // Make coins golden colored goldCoin.tint = 0xFFD700; // Add random movement with tween animation var randomX = goldCoin.x + (Math.random() - 0.5) * 200; var randomY = goldCoin.y + Math.random() * 150 + 50; tween(goldCoin, { x: randomX, y: randomY, rotation: Math.PI * 4 }, { duration: 800 + Math.random() * 400, easing: tween.easeOut }); coins.push(goldCoin); game.addChild(goldCoin); } // Create burning effect with flames tween(obstacle, { tint: 0x000000, alpha: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { obstacle.isDestroyed = true; // Mark as destroyed for flame cleanup obstacle.destroy(); // Remove from obstacles array after animation completes var index = obstacles.indexOf(obstacle); if (index > -1) { obstacles.splice(index, 1); } } }); break; } } } // Update day/night cycle dayNightTimer++; if (dayNightTimer >= dayNightCycleDuration) { transitionDayNight(); dayNightTimer = 0; } // Police exhaust particles now stay in their natural layer order // Road color no longer changes automatically }; // Road color change function (kept for potential future use) function changeRoadColor(newColor) { roadBackground.tint = newColor; } // Day/night cycle transition function function transitionDayNight() { if (isTransitioning) return; isTransitioning = true; isDay = !isDay; var targetColors = isDay ? dayColors : nightColors; var transitionDuration = 2000; // 2 seconds transition // Increment cycle count and apply speed boost when transitioning to day (completing full cycle) if (isDay) { cycleCount++; cycleSpeedBoost = cycleCount * speedBoostPerCycle; } // Immediately update game speed for the new cycle var currentBaseSpeed = Math.min(maxSpeed, baseGameSpeed + gameTimer * speedIncrement + cycleSpeedBoost); gameSpeed = isDay ? currentBaseSpeed : currentBaseSpeed * nightSpeedMultiplier; // Transition background color tween(game, { backgroundColor: targetColors.sky }, { duration: transitionDuration, easing: tween.easeInOut }); // Transition forest backgrounds tween(leftForest, { tint: targetColors.forest }, { duration: transitionDuration, easing: tween.easeInOut }); tween(rightForest, { tint: targetColors.forest }, { duration: transitionDuration, easing: tween.easeInOut }); // Transition road background tween(roadBackground, { tint: targetColors.road }, { duration: transitionDuration, easing: tween.easeInOut, onFinish: function onFinish() { isTransitioning = false; } }); // Adjust trees brightness for night/day var treeBrightness = isDay ? 1.0 : 0.6; for (var i = 0; i < trees.length; i++) { var tree = trees[i]; tween(tree, { alpha: treeBrightness }, { duration: transitionDuration, easing: tween.easeInOut }); } // Adjust trees2 brightness for night/day for (var i2 = 0; i2 < trees2.length; i2++) { var tree2 = trees2[i2]; tween(tree2, { alpha: treeBrightness }, { duration: transitionDuration, easing: tween.easeInOut }); } // Adjust flowers brightness for night/day var flowerBrightness = isDay ? 1.0 : 0.5; for (var f = 0; f < flowers.length; f++) { var flower = flowers[f]; tween(flower, { alpha: flowerBrightness }, { duration: transitionDuration, easing: tween.easeInOut }); } // Control player headlights based on day/night var headlightAlpha = isDay ? 0 : 0.8; tween(player.leftHeadlight, { alpha: headlightAlpha }, { duration: transitionDuration, easing: tween.easeInOut }); tween(player.rightHeadlight, { alpha: headlightAlpha }, { duration: transitionDuration, easing: tween.easeInOut }); } // Spawn initial powerup at game start spawnPowerUp(); // Initialize first rain cycle rainCooldown = 1800 + Math.random() * 9000; // 30 seconds to 3 minutes before first rain nextRainEvent = 0; // Start background music - this will be the base music layer LK.playMusic('bgmusic', { fade: { start: 0, end: 1, duration: 1000 } }); ;
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = -25;
self.update = function () {
self.y += self.speed;
};
return self;
});
var Coin = Container.expand(function () {
var self = Container.call(this);
var coinGraphics = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = gameSpeed;
self.collected = false;
self.update = function () {
self.y += self.speed;
self.rotation += 0.1;
};
return self;
});
var DebrisParticle = Container.expand(function () {
var self = Container.call(this);
var debrisGraphics = self.attachAsset('debrisParticle', {
anchorX: 0.5,
anchorY: 0.5
});
self.speedX = (Math.random() - 0.5) * 20; // Random horizontal speed
self.speedY = -5 - Math.random() * 15; // Upward initial speed
self.gravity = 0.8; // Gravity acceleration
self.lifetime = 90 + Math.random() * 60; // 1.5-2.5 seconds
self.maxLifetime = self.lifetime;
self.rotationSpeed = (Math.random() - 0.5) * 0.3;
// Random colors for debris (metal, glass, etc.)
var debrisColors = [0x666666, 0x888888, 0x444444, 0x999999, 0x333333];
debrisGraphics.tint = debrisColors[Math.floor(Math.random() * debrisColors.length)];
self.update = function () {
// Apply physics
self.x += self.speedX;
self.y += self.speedY;
self.speedY += self.gravity; // Apply gravity
self.rotation += self.rotationSpeed;
self.lifetime--;
// Fade out over time
var fadeProgress = 1 - self.lifetime / self.maxLifetime;
self.alpha = Math.max(0, 1 - fadeProgress * 1.5);
};
return self;
});
var ExhaustParticle = Container.expand(function () {
var self = Container.call(this);
var particleGraphics = self.attachAsset('exhaustParticle', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 3 + Math.random() * 2;
self.sideSpeed = (Math.random() - 0.5) * 2;
self.lifetime = 60 + Math.random() * 30;
self.maxLifetime = self.lifetime;
self.update = function () {
self.y += self.speed;
self.x += self.sideSpeed;
self.lifetime--;
// Fade out and scale up over time
var fadeProgress = 1 - self.lifetime / self.maxLifetime;
self.alpha = Math.max(0, 1 - fadeProgress * 1.5);
self.scaleX = 1 + fadeProgress * 0.8;
self.scaleY = 1 + fadeProgress * 0.8;
};
return self;
});
var Flame = Container.expand(function () {
var self = Container.call(this);
var flameGraphics = self.attachAsset('flame', {
anchorX: 0.5,
anchorY: 1.0
});
self.speed = -2 - Math.random() * 3;
self.sideSpeed = (Math.random() - 0.5) * 4;
self.lifetime = 30 + Math.random() * 20;
self.maxLifetime = self.lifetime;
self.flickerTimer = 0;
self.parentCar = null; // Reference to the police car this flame belongs to
self.offsetX = 0; // Offset from car position
self.offsetY = 0; // Offset from car position
self.update = function () {
// If attached to a police car, follow its position
if (self.parentCar && !self.parentCar.isDestroyed) {
self.x = self.parentCar.x + self.offsetX;
self.y = self.parentCar.y + self.offsetY;
}
self.lifetime--;
// Flicker effect
self.flickerTimer++;
if (self.flickerTimer % 3 === 0) {
var colors = [0xff4500, 0xff6600, 0xff8800, 0xffaa00];
flameGraphics.tint = colors[Math.floor(Math.random() * colors.length)];
}
// Fade and scale over time
var fadeProgress = 1 - self.lifetime / self.maxLifetime;
self.alpha = Math.max(0, 1 - fadeProgress * 1.2);
self.scaleX = 1.5 + fadeProgress * 1.2; // Increased base size from 0.5 to 1.5
self.scaleY = 2.0 + fadeProgress * 2.0; // Increased base size from 0.8 to 2.0
};
return self;
});
var Flower = Container.expand(function () {
var self = Container.call(this);
var flowerGraphics = self.attachAsset('flower', {
anchorX: 0.5,
anchorY: 1.0
});
self.speed = gameSpeed;
self.update = function () {
self.y += self.speed;
};
return self;
});
var HeartPowerUp = Container.expand(function () {
var self = Container.call(this);
var heartGraphics = self.attachAsset('heart', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = gameSpeed;
self.collected = false;
self.lifetime = 420; // 7 seconds at 60fps
self.update = function () {
self.y += self.speed;
self.rotation += 0.1;
// Pulsing effect
self.scaleX = 1 + Math.sin(LK.ticks * 0.1) * 0.1;
self.scaleY = 1 + Math.sin(LK.ticks * 0.1) * 0.1;
// Decrease lifetime
self.lifetime--;
// Flash when about to disappear (last 3 seconds)
if (self.lifetime <= 180 && self.lifetime > 0) {
self.alpha = (Math.sin(self.lifetime * 0.3) + 1) * 0.5;
}
};
return self;
});
var LaneLine = Container.expand(function () {
var self = Container.call(this);
var lineGraphics = self.attachAsset('laneLine', {
anchorX: 0.5,
anchorY: 0.5
});
// Make lane lines semi-transparent so they appear behind cars
lineGraphics.alpha = 0.7;
self.speed = gameSpeed;
self.update = function () {
self.y += self.speed;
};
return self;
});
var MovingBarrier = Container.expand(function () {
var self = Container.call(this);
var barrierGraphics = self.attachAsset('movingBarrier', {
anchorX: 0.5,
anchorY: 1.0
});
self.speed = gameSpeed;
self.horizontalSpeed = 2; // Speed of horizontal movement
self.direction = 1; // 1 for right, -1 for left
self.lanes = [580, 1024, 1468]; // Reference to lane positions
self.startLaneIndex = 1; // Start in middle lane by default
self.minX = 580; // Will be set based on start lane
self.maxX = 1468; // Will be set based on start lane
self.changeDirectionTimer = 0;
self.changeDirectionDelay = 120 + Math.random() * 180; // 2-5 seconds
// Function to set movement boundaries for one lane change only
self.setMovementBounds = function (startLaneIndex) {
self.startLaneIndex = startLaneIndex;
if (startLaneIndex === 0) {
// Left lane - can only move to middle lane
self.minX = self.lanes[0];
self.maxX = self.lanes[1];
} else if (startLaneIndex === 1) {
// Middle lane - can move to left or right lane
var canMoveLeft = Math.random() < 0.5;
if (canMoveLeft) {
self.minX = self.lanes[0];
self.maxX = self.lanes[1];
self.direction = -1; // Start moving left
} else {
self.minX = self.lanes[1];
self.maxX = self.lanes[2];
self.direction = 1; // Start moving right
}
} else {
// Right lane - can only move to middle lane
self.minX = self.lanes[1];
self.maxX = self.lanes[2];
}
};
self.update = function () {
self.y += self.speed;
// Move horizontally
self.x += self.horizontalSpeed * self.direction;
// Bounce off lane boundaries with smooth animation
if (self.x <= self.minX) {
self.x = self.minX;
self.direction = 1; // Change to move right
// Add bounce animation
tween(self, {
scaleX: 2.6,
rotation: 0.1
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 2.4,
rotation: 0
}, {
duration: 200,
easing: tween.easeInOut
});
}
});
} else if (self.x >= self.maxX) {
self.x = self.maxX;
self.direction = -1; // Change to move left
// Add bounce animation
tween(self, {
scaleX: 2.6,
rotation: -0.1
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 2.4,
rotation: 0
}, {
duration: 200,
easing: tween.easeInOut
});
}
});
}
// Random direction changes for unpredictability
self.changeDirectionTimer++;
if (self.changeDirectionTimer >= self.changeDirectionDelay) {
self.direction *= -1; // Reverse direction
self.changeDirectionTimer = 0;
self.changeDirectionDelay = 120 + Math.random() * 180; // New random delay
// Add direction change animation
tween(self, {
scaleY: 2.6
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleY: 2.4
}, {
duration: 150,
easing: tween.easeInOut
});
}
});
}
};
return self;
});
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obstacleGraphics = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 1.0
});
self.speed = gameSpeed;
// Add police lights to the obstacle (police car) - properly attached to self container
// Main roof lights - positioned on top of the police car
var leftLight = self.attachAsset('policeLight', {
anchorX: 0.5,
anchorY: 0.5
});
leftLight.x = -20;
leftLight.y = -130;
var rightLight = self.attachAsset('policeLight', {
anchorX: 0.5,
anchorY: 0.5
});
rightLight.x = 20;
rightLight.y = -130;
rightLight.tint = 0x0000ff; // Make right light blue
// Side mirror lights
var leftSideLight = self.attachAsset('policeLight', {
anchorX: 0.5,
anchorY: 0.5
});
leftSideLight.x = -35;
leftSideLight.y = -100;
var rightSideLight = self.attachAsset('policeLight', {
anchorX: 0.5,
anchorY: 0.5
});
rightSideLight.x = 35;
rightSideLight.y = -100;
rightSideLight.tint = 0x0000ff; // Make right side light blue
// Front bumper lights
var topLeftLight = self.attachAsset('policeLight', {
anchorX: 0.5,
anchorY: 0.5
});
topLeftLight.x = -25;
topLeftLight.y = -145;
var topRightLight = self.attachAsset('policeLight', {
anchorX: 0.5,
anchorY: 0.5
});
topRightLight.x = 25;
topRightLight.y = -145;
topRightLight.tint = 0x0000ff; // Make top right light blue
// Store light references for animation
self.leftLight = leftLight;
self.rightLight = rightLight;
self.leftSideLight = leftSideLight;
self.rightSideLight = rightSideLight;
self.topLeftLight = topLeftLight;
self.topRightLight = topRightLight;
self.lightTimer = 0;
self.isRedActive = true;
// Lane changing properties
self.canChangeLanes = false;
self.laneChangeTimer = 0;
self.laneChangeDelay = 180 + Math.random() * 240; // 3-7 seconds random delay
self.targetLaneIndex = Math.floor(Math.random() * 3); // Random target lane
self.isChangingLanes = false;
self.originalLaneIndex = 0; // Will be set when spawned
self.update = function () {
self.y += self.speed;
// Handle lane changing behavior - only at night
if (self.canChangeLanes && !self.isChangingLanes && self.y > 200 && self.y < 2000 && !isDay) {
self.laneChangeTimer++;
if (self.laneChangeTimer >= self.laneChangeDelay) {
// Start lane change
self.isChangingLanes = true;
var currentLaneIndex = self.originalLaneIndex;
// Choose a different lane
var availableLanes = [];
for (var i = 0; i < 3; i++) {
if (i !== currentLaneIndex) {
availableLanes.push(i);
}
}
if (availableLanes.length > 0) {
var newLaneIndex = availableLanes[Math.floor(Math.random() * availableLanes.length)];
var targetX = player.lanes[newLaneIndex];
// Animate lane change using tween
tween(self, {
x: targetX
}, {
duration: 1000 + Math.random() * 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
self.isChangingLanes = false;
self.originalLaneIndex = newLaneIndex;
// Set new delay for next potential lane change
self.laneChangeTimer = 0;
self.laneChangeDelay = 300 + Math.random() * 600; // 5-15 seconds
}
});
}
}
}
// Police lights visible during day and night, more prominent at night
var dayAlpha = isDay ? 0.6 : 1.0; // Visible during day, full at night
var dayAlphaLow = isDay ? 0.3 : 0.5; // Reduced for inactive light but still visible
// Animate police lights with enhanced tween effects
self.lightTimer++;
if (self.lightTimer >= 15) {
// Change every 15 frames (0.25 seconds at 60fps)
self.lightTimer = 0;
self.isRedActive = !self.isRedActive;
if (self.isRedActive) {
// Red lights active - animate with pulse effect
tween(self.leftLight, {
alpha: dayAlpha,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut
});
tween(self.leftSideLight, {
alpha: dayAlpha,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut
});
tween(self.topLeftLight, {
alpha: dayAlpha,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut
});
// Blue lights dimmed
tween(self.rightLight, {
alpha: dayAlphaLow,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeOut
});
tween(self.rightSideLight, {
alpha: dayAlphaLow,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeOut
});
tween(self.topRightLight, {
alpha: dayAlphaLow,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeOut
});
} else {
// Blue lights active - animate with pulse effect
tween(self.rightLight, {
alpha: dayAlpha,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut
});
tween(self.rightSideLight, {
alpha: dayAlpha,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut
});
tween(self.topRightLight, {
alpha: dayAlpha,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut
});
// Red lights dimmed
tween(self.leftLight, {
alpha: dayAlphaLow,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeOut
});
tween(self.leftSideLight, {
alpha: dayAlphaLow,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeOut
});
tween(self.topLeftLight, {
alpha: dayAlphaLow,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeOut
});
}
}
};
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 1.0
});
// Add headlights to the player car - positioned at the front of the vehicle
var leftHeadlight = self.attachAsset('headlight', {
anchorX: 0.5,
anchorY: 0.5
});
leftHeadlight.x = -25;
leftHeadlight.y = -150; // Moved further forward to the front of the car
leftHeadlight.alpha = 0; // Start invisible
var rightHeadlight = self.attachAsset('headlight', {
anchorX: 0.5,
anchorY: 0.5
});
rightHeadlight.x = 25;
rightHeadlight.y = -150; // Moved further forward to the front of the car
rightHeadlight.alpha = 0; // Start invisible
// Store headlight references
self.leftHeadlight = leftHeadlight;
self.rightHeadlight = rightHeadlight;
// Move headlights to front of all elements
self.moveHeadlightsToFront = function () {
if (self.parent) {
// Remove headlights from current position
self.removeChild(leftHeadlight);
self.removeChild(rightHeadlight);
// Add them back to the front (last children render on top)
self.addChild(leftHeadlight);
self.addChild(rightHeadlight);
}
};
self.groundY = 2300;
self.lanes = [580, 1024, 1468]; // Three lanes adjusted for wider road
self.currentLane = 1; // Middle lane
self.targetX = self.lanes[self.currentLane];
self.x = self.targetX;
self.y = self.groundY;
self.moveLeft = function () {
if (self.currentLane > 0) {
self.currentLane--;
self.targetX = self.lanes[self.currentLane];
// Add shake effect when moving left
tween(self, {
rotation: -0.1
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
rotation: 0
}, {
duration: 100,
easing: tween.easeInOut
});
}
});
// Play car movement sound
try {
var carMoveSound = LK.getSound('carMove');
carMoveSound.volume = 0.005;
carMoveSound.play();
} catch (e) {
console.log('Car move sound error:', e);
}
}
};
self.moveRight = function () {
if (self.currentLane < 2) {
self.currentLane++;
self.targetX = self.lanes[self.currentLane];
// Add shake effect when moving right
tween(self, {
rotation: 0.1
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
rotation: 0
}, {
duration: 100,
easing: tween.easeInOut
});
}
});
// Play car movement sound
try {
var carMoveSound = LK.getSound('carMove');
carMoveSound.volume = 0.005;
carMoveSound.play();
} catch (e) {
console.log('Car move sound error:', e);
}
}
};
self.update = function () {
// Handle lane switching
var dx = self.targetX - self.x;
if (Math.abs(dx) > 2) {
self.x += dx * 0.25;
} else {
self.x = self.targetX;
}
// Ensure headlights are always in front
self.moveHeadlightsToFront();
};
return self;
});
var PoliceChaser = Container.expand(function () {
var self = Container.call(this);
var policeGraphics = self.attachAsset('policeChaser', {
anchorX: 0.5,
anchorY: 1.0
});
// Add police lights - same as regular obstacles
var leftLight = self.attachAsset('policeLight', {
anchorX: 0.5,
anchorY: 0.5
});
leftLight.x = -20;
leftLight.y = -130;
var rightLight = self.attachAsset('policeLight', {
anchorX: 0.5,
anchorY: 0.5
});
rightLight.x = 20;
rightLight.y = -130;
rightLight.tint = 0x0000ff;
// Store light references for animation
self.leftLight = leftLight;
self.rightLight = rightLight;
self.lightTimer = 0;
self.isRedActive = true;
// Chase behavior properties
self.followDistance = 500; // Distance to maintain behind player
self.maxFollowDistance = 700; // Maximum distance to prevent getting too close
self.minFollowDistance = 400; // Minimum distance to maintain
self.speed = gameSpeed;
self.targetX = 1024; // Center lane by default
self.lastPlayerLane = 1; // Track player's last lane
self.update = function () {
// Calculate dynamic follow distance based on current distance to player
var currentDistance = self.y - player.y;
var targetDistance = self.followDistance;
// If too close, increase follow distance
if (currentDistance < self.minFollowDistance) {
targetDistance = self.maxFollowDistance;
} else if (currentDistance > self.maxFollowDistance) {
targetDistance = self.minFollowDistance;
}
// Smoothly adjust position to maintain proper distance
var distanceDiff = currentDistance - targetDistance;
self.y = player.y + targetDistance + distanceDiff * 0.95; // Gradual adjustment
// Follow player's lane with slight delay for realistic behavior
var currentPlayerLane = player.currentLane;
if (currentPlayerLane !== self.lastPlayerLane) {
// Player changed lanes, start following
self.targetX = player.lanes[currentPlayerLane];
self.lastPlayerLane = currentPlayerLane;
}
// Smooth lane following
var dx = self.targetX - self.x;
if (Math.abs(dx) > 5) {
self.x += dx * 0.08; // Slower than player for realistic chase
} else {
self.x = self.targetX;
}
// Ensure police lights are always in front
self.removeChild(self.leftLight);
self.removeChild(self.rightLight);
self.addChild(self.leftLight);
self.addChild(self.rightLight);
// Animate police lights
self.lightTimer++;
if (self.lightTimer >= 15) {
self.lightTimer = 0;
self.isRedActive = !self.isRedActive;
var dayAlpha = isDay ? 0.8 : 1.0;
var dayAlphaLow = isDay ? 0.4 : 0.6;
if (self.isRedActive) {
// Red light active
tween(self.leftLight, {
alpha: dayAlpha,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut
});
tween(self.rightLight, {
alpha: dayAlphaLow,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeOut
});
} else {
// Blue light active
tween(self.rightLight, {
alpha: dayAlpha,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 100,
easing: tween.easeOut
});
tween(self.leftLight, {
alpha: dayAlphaLow,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeOut
});
}
}
};
return self;
});
var PowerUp = Container.expand(function () {
var self = Container.call(this);
var powerupGraphics = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = gameSpeed;
self.collected = false;
self.lifetime = 420; // 7 seconds at 60fps
self.update = function () {
self.y += self.speed;
self.rotation += 0.15;
// Decrease lifetime
self.lifetime--;
// Flash when about to disappear (last 3 seconds)
if (self.lifetime <= 180 && self.lifetime > 0) {
self.alpha = (Math.sin(self.lifetime * 0.3) + 1) * 0.5;
}
};
return self;
});
var RainDrop = Container.expand(function () {
var self = Container.call(this);
var dropGraphics = self.attachAsset('rainDrop', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 15 + Math.random() * 10; // Rain falls fast
self.sideSpeed = -2 + Math.random() * 4; // Slight horizontal movement
self.alpha = 0.3 + Math.random() * 0.4; // Semi-transparent
self.update = function () {
self.y += self.speed;
self.x += self.sideSpeed;
};
return self;
});
var Smoke = Container.expand(function () {
var self = Container.call(this);
var smokeGraphics = self.attachAsset('smoke', {
anchorX: 0.5,
anchorY: 1.0
});
self.speed = -1 - Math.random() * 2;
self.sideSpeed = (Math.random() - 0.5) * 3;
self.lifetime = 60 + Math.random() * 40;
self.maxLifetime = self.lifetime;
self.update = function () {
self.y += self.speed;
self.x += self.sideSpeed;
self.lifetime--;
// Fade and expand over time
var fadeProgress = 1 - self.lifetime / self.maxLifetime;
self.alpha = Math.max(0, 0.8 - fadeProgress * 1.2);
self.scaleX = 0.3 + fadeProgress * 2;
self.scaleY = 0.3 + fadeProgress * 2;
};
return self;
});
var Tree = Container.expand(function () {
var self = Container.call(this);
var treeGraphics = self.attachAsset('tree', {
anchorX: 0.5,
anchorY: 1.0
});
self.speed = gameSpeed;
self.update = function () {
self.y += self.speed;
};
return self;
});
var Tree2 = Container.expand(function () {
var self = Container.call(this);
var treeGraphics = self.attachAsset('tree2', {
anchorX: 0.5,
anchorY: 1.0
});
self.speed = gameSpeed;
self.update = function () {
self.y += self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87CEEB
});
/****
* Game Code
****/
var player;
var obstacles = [];
var coins = [];
var powerups = [];
var heartPowerups = [];
var bullets = [];
var movingBarriers = [];
var hasWeapon = false;
var weaponTimer = 0;
var weaponDuration = 1200; // 20 seconds at 60fps
var heartPowerupSpawnTimer = 0;
var movingBarrierSpawnTimer = 0;
var laneLines = [];
var trees = [];
var trees2 = [];
var flowers = [];
var policeChaser = null;
var policeChaserActive = false;
var policeChaserTimer = 0;
var policeChaserActivationTime = 900; // 15 seconds at 60fps (15 * 60)
var policeChaserChaseDuration = 0; // Will be set randomly when activated
var policeChaserCooldownDuration = 0; // Will be set randomly when deactivated
var policeChaserTimeWindow = 3600; // 1 minute window (60 seconds at 60fps)
var policeChaserMaxChaseTime = 600; // 10 seconds maximum chase time per window (10 * 60fps)
var policeChaserUsedTime = 0; // Time used in current window
var policeChaserWindowTimer = 0; // Timer for the current 1-minute window
var gameSpeed = 8;
var maxSpeed = 20;
var speedIncrement = 0.005;
var distance = 0;
var obstacleSpawnTimer = 0;
var coinSpawnTimer = 0;
var powerupSpawnTimer = 0;
var laneLineSpawnTimer = 0;
var treeSpawnTimer = 0;
var lastSwipeX = 0;
var lastSwipeY = 0;
var swipeStarted = false;
var policeMusicPlaying = false;
var policeOnScreen = false;
var lives = 3;
var maxLives = 10;
var exhaustParticles = [];
var exhaustSpawnTimer = 0;
var flames = [];
var smokes = [];
var debrisParticles = [];
var rainDrops = [];
var rainSpawnTimer = 0;
// Rain cycle variables
var isRaining = false;
var rainTimer = 0;
var rainDuration = 0; // Will be set randomly
var rainCooldown = 0; // Will be set randomly
var nextRainEvent = 0; // Timer for next rain event
var rainSoundPlaying = false;
var thunderTimer = 0;
var nextThunderTime = 0;
// Day/night cycle variables
var dayNightTimer = 0;
var dayNightCycleDuration = 3600; // 60 seconds (1 minute) at 60fps
var isDay = true;
var isTransitioning = false;
var cycleCount = 0; // Track completed day/night cycles
var cycleSpeedBoost = 0; // Additional speed from completed cycles
var speedBoostPerCycle = 2; // Speed increase per completed cycle
// Game timer for police lane changing feature
var gameTimer = 0;
var laneChangeStartTime = 7200; // 2 minutes at 60fps (2 * 60 * 60)
// Night speed multiplier
var nightSpeedMultiplier = 1.25; // 25% faster during night
var baseGameSpeed = 8; // Store the base game speed
var dayColors = {
sky: 0x87CEEB,
// Light blue sky
forest: 0x228b22,
// Forest green
road: 0xffffff // Normal road color
};
var nightColors = {
sky: 0x191970,
// Midnight blue
forest: 0x0f2f0f,
// Dark forest green
road: 0x404040 // Darker road color
};
// Create road background - widened
var roadBackground = game.addChild(LK.getAsset('roadBackground', {
anchorX: 0.5,
anchorY: 0.5
}));
roadBackground.x = 1024; // Center of screen
roadBackground.y = 1366;
roadBackground.scaleX = 1.3; // Widen road by 30%
// Create green forest backgrounds on sides - narrowed
var leftForest = game.addChild(LK.getAsset('forestBackground', {
anchorX: 0.5,
anchorY: 0.5
}));
leftForest.x = 190; // Moved closer to center - narrower area
leftForest.y = 1366;
leftForest.scaleX = 0.8; // Make forest area narrower
var rightForest = game.addChild(LK.getAsset('forestBackground', {
anchorX: 0.5,
anchorY: 0.5
}));
rightForest.x = 1858; // Moved closer to center - narrower area
rightForest.y = 1366;
rightForest.scaleX = 0.8; // Make forest area narrower
// Create road borders - adjusted for wider road
var leftBorder = game.addChild(LK.getAsset('roadBorder', {
anchorX: 0.5,
anchorY: 0.5
}));
leftBorder.x = 390; // Moved outward for wider road
leftBorder.y = 1366;
var rightBorder = game.addChild(LK.getAsset('roadBorder', {
anchorX: 0.5,
anchorY: 0.5
}));
rightBorder.x = 1658; // Moved outward for wider road
rightBorder.y = 1366;
// Spawn initial lane lines to establish proper layering
spawnLaneLine();
// Create player
player = game.addChild(new Player());
player.scaleX = 2.4;
player.scaleY = 2.4;
// Create police chaser that follows the player (initially hidden)
policeChaser = game.addChild(new PoliceChaser());
policeChaser.scaleX = 2.2; // Slightly smaller than player
policeChaser.scaleY = 2.2;
policeChaser.x = player.x; // Start in same lane as player
policeChaser.visible = false; // Start hidden
policeChaser.alpha = 0; // Start transparent
// Create score display
var scoreText = new Text2('Score: 0', {
size: 60,
fill: 0xFFFFFF
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
// Create health bar label
var healthLabel = new Text2('Lives:', {
size: 40,
fill: 0xFFFFFF
});
healthLabel.anchor.set(1, 0.5);
healthLabel.x = -200;
healthLabel.y = 100;
LK.gui.topRight.addChild(healthLabel);
// Distance display removed - only counting coins for score
// Create lives display as hearts in top right with better styling
var healthBars = [];
for (var h = 0; h < maxLives; h++) {
var heart = LK.getAsset('heart', {
anchorX: 0.5,
anchorY: 0.5
});
// Arrange hearts in two rows for better space usage with 10 hearts
var row = Math.floor(h / 5); // 5 hearts per row
var col = h % 5; // Column position within row
heart.x = -80 - col * 50; // Closer spacing for more hearts
heart.y = 80 + row * 50; // Two rows: first at y=80, second at y=130
heart.scaleX = 1.0; // Slightly smaller to fit more hearts
heart.scaleY = 1.0;
// Add a subtle glow effect
heart.alpha = 0.9;
// Only show the first 3 hearts initially (player starts with 3 lives)
if (h >= 3) {
heart.visible = false;
heart.alpha = 0;
}
LK.gui.topRight.addChild(heart);
healthBars.push(heart);
}
// Touch controls
game.down = function (x, y, obj) {
lastSwipeX = x;
lastSwipeY = y;
swipeStarted = true;
};
game.up = function (x, y, obj) {
if (swipeStarted) {
var deltaX = x - lastSwipeX;
var deltaY = y - lastSwipeY;
var swipeThreshold = 100;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal swipe
if (deltaX > swipeThreshold) {
player.moveRight();
} else if (deltaX < -swipeThreshold) {
player.moveLeft();
}
}
}
swipeStarted = false;
};
function spawnObstacle() {
var obstacle = new Obstacle();
var laneIndex = Math.floor(Math.random() * 3);
obstacle.x = player.lanes[laneIndex];
obstacle.y = -100;
obstacle.speed = gameSpeed;
obstacle.scaleX = 0.9;
obstacle.scaleY = 0.9;
// Set original lane index for lane changing
obstacle.originalLaneIndex = laneIndex;
// Enable lane changing after 2 minutes if game has been running long enough
if (gameTimer >= laneChangeStartTime) {
obstacle.canChangeLanes = Math.random() < 0.3; // 30% chance to be a lane changer
}
tween(obstacle, {
scaleX: 2.4,
scaleY: 2.4
}, {
duration: 400,
easing: tween.easeOut
});
obstacles.push(obstacle);
game.addChild(obstacle);
}
function spawnCoin() {
var coin = new Coin();
var laneIndex = Math.floor(Math.random() * 3);
var targetX = player.lanes[laneIndex];
var canSpawn = true;
// Check if there's an obstacle in this lane that would conflict
for (var i = 0; i < obstacles.length; i++) {
var obstacle = obstacles[i];
// Check if obstacle is in the same lane and close to spawn area
if (Math.abs(obstacle.x - targetX) < 50 && obstacle.y >= -300 && obstacle.y <= 200) {
canSpawn = false;
break;
}
}
// Check if player is in this lane and close to spawn area
if (canSpawn) {
if (Math.abs(player.x - targetX) < 100 && player.y >= 2000) {
canSpawn = false;
}
}
// Also check if there's a powerup in this lane that would conflict
if (canSpawn) {
for (var p = 0; p < powerups.length; p++) {
var powerup = powerups[p];
// Check if powerup is in the same lane and close to spawn area
if (Math.abs(powerup.x - targetX) < 50 && powerup.y >= -300 && powerup.y <= 200) {
canSpawn = false;
break;
}
}
}
// If we can't spawn in the selected lane, try other lanes
if (!canSpawn) {
var availableLanes = [];
for (var lane = 0; lane < 3; lane++) {
var laneX = player.lanes[lane];
var laneAvailable = true;
// Check for obstacles in this lane
for (var j = 0; j < obstacles.length; j++) {
var obs = obstacles[j];
if (Math.abs(obs.x - laneX) < 50 && obs.y >= -300 && obs.y <= 200) {
laneAvailable = false;
break;
}
}
// Check for player in this lane
if (laneAvailable) {
if (Math.abs(player.x - laneX) < 100 && player.y >= 2000) {
laneAvailable = false;
}
}
// Also check for powerups in this lane
if (laneAvailable) {
for (var k = 0; k < powerups.length; k++) {
var pup = powerups[k];
if (Math.abs(pup.x - laneX) < 50 && pup.y >= -300 && pup.y <= 200) {
laneAvailable = false;
break;
}
}
}
if (laneAvailable) {
availableLanes.push(lane);
}
}
// If no lanes are available, don't spawn coin this time
if (availableLanes.length === 0) {
return;
}
// Pick a random available lane
laneIndex = availableLanes[Math.floor(Math.random() * availableLanes.length)];
targetX = player.lanes[laneIndex];
}
coin.x = targetX;
coin.y = -50;
coin.speed = gameSpeed;
coin.scaleX = 0.5;
coin.scaleY = 0.5;
tween(coin, {
scaleX: 1,
scaleY: 1
}, {
duration: 500,
easing: tween.easeOut
});
coins.push(coin);
game.addChild(coin);
}
function spawnPowerUp() {
var powerup = new PowerUp();
var laneIndex = Math.floor(Math.random() * 3);
var targetX = player.lanes[laneIndex];
var canSpawn = true;
// Check if there's an obstacle in this lane that would conflict
for (var i = 0; i < obstacles.length; i++) {
var obstacle = obstacles[i];
// Check if obstacle is in the same lane and close to spawn area
if (Math.abs(obstacle.x - targetX) < 80 && obstacle.y >= -400 && obstacle.y <= 300) {
canSpawn = false;
break;
}
}
// If we can't spawn in the selected lane, try other lanes
if (!canSpawn) {
var availableLanes = [];
for (var lane = 0; lane < 3; lane++) {
var laneX = player.lanes[lane];
var laneAvailable = true;
// Check for obstacles in this lane
for (var j = 0; j < obstacles.length; j++) {
var obs = obstacles[j];
if (Math.abs(obs.x - laneX) < 80 && obs.y >= -400 && obs.y <= 300) {
laneAvailable = false;
break;
}
}
if (laneAvailable) {
availableLanes.push(lane);
}
}
// If no lanes are available, don't spawn powerup this time
if (availableLanes.length === 0) {
return;
}
// Pick a random available lane
laneIndex = availableLanes[Math.floor(Math.random() * availableLanes.length)];
targetX = player.lanes[laneIndex];
}
powerup.x = targetX;
powerup.y = -50;
powerup.speed = gameSpeed;
powerup.scaleX = 0.3;
powerup.scaleY = 0.3;
tween(powerup, {
scaleX: 1,
scaleY: 1
}, {
duration: 600,
easing: tween.easeOut
});
powerups.push(powerup);
game.addChild(powerup);
}
function spawnLaneLine() {
// Lane dividers between lanes - adjusted for wider road
var positions = [802, 1246]; // Between lanes 0-1 and 1-2, proportionally spaced
for (var i = 0; i < positions.length; i++) {
var laneLine = new LaneLine();
laneLine.x = positions[i];
laneLine.y = -20;
laneLine.speed = gameSpeed;
laneLines.push(laneLine);
// Simply add lane lines normally - layering optimization removed for performance
game.addChild(laneLine);
}
}
function spawnExhaustParticle() {
var particle = new ExhaustParticle();
particle.x = player.x + (Math.random() - 0.5) * 40;
particle.y = player.y + 20;
particle.scaleX = 0.5 + Math.random() * 0.5;
particle.scaleY = 0.5 + Math.random() * 0.5;
particle.alpha = 0.6 + Math.random() * 0.4;
exhaustParticles.push(particle);
game.addChild(particle);
}
function createFlameEffect(x, y, parentCar) {
// Create multiple flame particles (optimized amount)
for (var i = 0; i < 8; i++) {
// Balanced flame count for performance
var flame = new Flame();
flame.x = x + (Math.random() - 0.5) * 80; // Increased spread from 60 to 80
flame.y = y + Math.random() * 40; // Increased spread from 30 to 40
flame.scaleX = 1.2 + Math.random() * 0.8; // Increased base size from 0.5 to 1.2
flame.scaleY = 1.2 + Math.random() * 0.8; // Increased base size from 0.5 to 1.2
// Attach flame to police car if provided
if (parentCar) {
flame.parentCar = parentCar;
flame.offsetX = (Math.random() - 0.5) * 80;
flame.offsetY = Math.random() * 40 - 60; // Flames above the car
}
flames.push(flame);
game.addChild(flame);
}
// Create smoke particles (optimized amount)
for (var j = 0; j < 4; j++) {
// Balanced smoke particle count
var smoke = new Smoke();
smoke.x = x + (Math.random() - 0.5) * 50; // Increased spread from 40 to 50
smoke.y = y - 10 + Math.random() * 30; // Increased spread from 20 to 30
smoke.alpha = 0.4 + Math.random() * 0.3;
smoke.scaleX = 1.5 + Math.random() * 0.5; // Increased smoke size
smoke.scaleY = 1.5 + Math.random() * 0.5; // Increased smoke size
smokes.push(smoke);
game.addChild(smoke);
}
// Create debris particles for explosion effect (optimized amount)
for (var k = 0; k < 10; k++) {
var debris = new DebrisParticle();
debris.x = x + (Math.random() - 0.5) * 40;
debris.y = y + (Math.random() - 0.5) * 30;
debris.scaleX = 0.5 + Math.random() * 1.0;
debris.scaleY = 0.5 + Math.random() * 1.0;
debrisParticles.push(debris);
game.addChild(debris);
}
}
function spawnTrees() {
// Spawn trees on left side of road - adjusted for narrower forest
var leftPositions = [120, 200, 280, 340];
for (var i = 0; i < leftPositions.length; i++) {
// Randomly choose between Tree and Tree2
if (Math.random() < 0.65) {
var leftTree;
if (Math.random() < 0.5) {
leftTree = new Tree();
trees.push(leftTree);
} else {
leftTree = new Tree2();
trees2.push(leftTree);
}
leftTree.x = leftPositions[i];
leftTree.y = -80;
leftTree.speed = gameSpeed;
// Add some variation in scale - bigger trees
leftTree.scaleX = 1.2 + Math.random() * 0.8;
leftTree.scaleY = 1.2 + Math.random() * 0.8;
// Add slight tint variation for natural look
var tintVariation = 0.9 + Math.random() * 0.2;
leftTree.tint = tintVariation * 0x90EE90 | 0;
// Apply current day/night brightness
if (!isDay) {
leftTree.alpha = 0.6;
}
// Simply add trees normally - layering optimization removed for performance
game.addChild(leftTree);
}
// Spawn flowers in left forest area
if (Math.random() < 0.25) {
var leftFlower = new Flower();
leftFlower.x = leftPositions[i] + (Math.random() - 0.5) * 80;
leftFlower.y = -40;
leftFlower.speed = gameSpeed;
leftFlower.scaleX = 0.8 + Math.random() * 0.4;
leftFlower.scaleY = 0.8 + Math.random() * 0.4;
// Add flower color variations
var flowerColors = [0xff69b4, 0xff1493, 0xffffff, 0xffff00, 0xff4500];
leftFlower.tint = flowerColors[Math.floor(Math.random() * flowerColors.length)];
if (!isDay) {
leftFlower.alpha = 0.5;
}
flowers.push(leftFlower);
game.addChild(leftFlower);
}
}
// Spawn trees on right side of road - adjusted for narrower forest
var rightPositions = [1708, 1788, 1868, 1928];
for (var j = 0; j < rightPositions.length; j++) {
// Randomly choose between Tree and Tree2
if (Math.random() < 0.65) {
var rightTree;
if (Math.random() < 0.5) {
rightTree = new Tree();
trees.push(rightTree);
} else {
rightTree = new Tree2();
trees2.push(rightTree);
}
rightTree.x = rightPositions[j];
rightTree.y = -80;
rightTree.speed = gameSpeed;
// Add some variation in scale - bigger trees
rightTree.scaleX = 1.2 + Math.random() * 0.8;
rightTree.scaleY = 1.2 + Math.random() * 0.8;
// Add slight tint variation for natural look
var tintVariation = 0.9 + Math.random() * 0.2;
rightTree.tint = tintVariation * 0x90EE90 | 0;
// Apply current day/night brightness
if (!isDay) {
rightTree.alpha = 0.6;
}
// Simply add trees normally - layering optimization removed for performance
game.addChild(rightTree);
}
// Spawn flowers in right forest area
if (Math.random() < 0.25) {
var rightFlower = new Flower();
rightFlower.x = rightPositions[j] + (Math.random() - 0.5) * 80;
rightFlower.y = -40;
rightFlower.speed = gameSpeed;
rightFlower.scaleX = 0.8 + Math.random() * 0.4;
rightFlower.scaleY = 0.8 + Math.random() * 0.4;
// Add flower color variations
var flowerColors = [0xff69b4, 0xff1493, 0xffffff, 0xffff00, 0xff4500];
rightFlower.tint = flowerColors[Math.floor(Math.random() * flowerColors.length)];
if (!isDay) {
rightFlower.alpha = 0.5;
}
flowers.push(rightFlower);
game.addChild(rightFlower);
}
}
}
function spawnHeartPowerUp() {
var heartPowerup = new HeartPowerUp();
var laneIndex = Math.floor(Math.random() * 3);
var targetX = player.lanes[laneIndex];
var canSpawn = true;
// Check if there's an obstacle in this lane that would conflict
for (var i = 0; i < obstacles.length; i++) {
var obstacle = obstacles[i];
// Check if obstacle is in the same lane and close to spawn area
if (Math.abs(obstacle.x - targetX) < 80 && obstacle.y >= -400 && obstacle.y <= 300) {
canSpawn = false;
break;
}
}
// If we can't spawn in the selected lane, try other lanes
if (!canSpawn) {
var availableLanes = [];
for (var lane = 0; lane < 3; lane++) {
var laneX = player.lanes[lane];
var laneAvailable = true;
// Check for obstacles in this lane
for (var j = 0; j < obstacles.length; j++) {
var obs = obstacles[j];
if (Math.abs(obs.x - laneX) < 80 && obs.y >= -400 && obs.y <= 300) {
laneAvailable = false;
break;
}
}
if (laneAvailable) {
availableLanes.push(lane);
}
}
// If no lanes are available, don't spawn heart powerup this time
if (availableLanes.length === 0) {
return;
}
// Pick a random available lane
laneIndex = availableLanes[Math.floor(Math.random() * availableLanes.length)];
targetX = player.lanes[laneIndex];
}
heartPowerup.x = targetX;
heartPowerup.y = -50;
heartPowerup.speed = gameSpeed;
heartPowerup.scaleX = 0.3;
heartPowerup.scaleY = 0.3;
tween(heartPowerup, {
scaleX: 1,
scaleY: 1
}, {
duration: 600,
easing: tween.easeOut
});
heartPowerups.push(heartPowerup);
game.addChild(heartPowerup);
}
function spawnMovingBarrier() {
var barrier = new MovingBarrier();
// Choose random starting lane
var startLaneIndex = Math.floor(Math.random() * 3);
barrier.x = player.lanes[startLaneIndex];
barrier.y = -100;
barrier.speed = gameSpeed;
barrier.scaleX = 0.9;
barrier.scaleY = 0.9;
// Set movement bounds for one lane change only
barrier.setMovementBounds(startLaneIndex);
// Animate barrier scaling up when spawned
tween(barrier, {
scaleX: 2.4,
scaleY: 2.4
}, {
duration: 400,
easing: tween.easeOut
});
movingBarriers.push(barrier);
game.addChild(barrier);
}
function spawnRain() {
// Spawn multiple rain drops across the screen width (optimized amount)
for (var i = 0; i < 6; i++) {
var rainDrop = new RainDrop();
rainDrop.x = Math.random() * 2048; // Random X across screen width
rainDrop.y = -20 - Math.random() * 50; // Start above screen
rainDrop.speed = gameSpeed + 15 + Math.random() * 10; // Rain speed based on game speed
rainDrops.push(rainDrop);
game.addChild(rainDrop);
}
}
// Main game loop
game.update = function () {
// Update game timer
gameTimer++;
// Handle police chaser timing and visibility with 1-minute window limit
policeChaserTimer++;
policeChaserWindowTimer++;
// Reset the time window every minute
if (policeChaserWindowTimer >= policeChaserTimeWindow) {
policeChaserWindowTimer = 0;
policeChaserUsedTime = 0; // Reset used time for new window
console.log('Police chaser time window reset - new 1 minute period');
}
if (!policeChaserActive) {
// Check if it's time to activate police chaser and if we have time left in current window
if (policeChaserTimer >= policeChaserActivationTime && policeChaserUsedTime < policeChaserMaxChaseTime) {
// Calculate remaining time in current window
var remainingTime = policeChaserMaxChaseTime - policeChaserUsedTime;
// Activate police chaser
policeChaserActive = true;
policeChaserTimer = 0;
// Set chase duration to minimum of remaining time or desired duration (max 10 seconds)
policeChaserChaseDuration = Math.min(remainingTime, 600); // Max 10 seconds or remaining time
// Position police chaser off-screen behind player and make visible
policeChaser.visible = true;
policeChaser.alpha = 0; // Start transparent for smooth fade in
policeChaser.y = player.y + 1200; // Start well behind player off-screen
policeChaser.x = player.x; // Start in same lane as player
// Fade in police chaser first
tween(policeChaser, {
alpha: 1
}, {
duration: 1500,
easing: tween.easeInOut
});
// Smoothly move police chaser into proper following position
tween(policeChaser, {
y: player.y + policeChaser.followDistance
}, {
duration: 3000,
// Slower entry for smoother appearance
easing: tween.easeInOut
});
console.log('Police chaser activated for', Math.floor(policeChaserChaseDuration / 60), 'seconds. Used time:', Math.floor(policeChaserUsedTime / 60), 'Remaining:', Math.floor(remainingTime / 60));
}
} else {
// Police chaser is active, track used time and check if chase duration is over
policeChaserUsedTime++; // Track time being used
if (policeChaserTimer >= policeChaserChaseDuration || policeChaserUsedTime >= policeChaserMaxChaseTime) {
// Deactivate police chaser
policeChaserActive = false;
policeChaserTimer = 0;
// Set cooldown duration - wait until next window if we've used all time
if (policeChaserUsedTime >= policeChaserMaxChaseTime) {
// Used all time in window, wait until window resets
var timeUntilWindowReset = policeChaserTimeWindow - policeChaserWindowTimer;
policeChaserActivationTime = timeUntilWindowReset + 300; // Wait for window reset + 5 seconds
console.log('Police chaser time limit reached. Waiting', Math.floor(timeUntilWindowReset / 60), 'seconds for window reset');
} else {
// Still have time left, set normal cooldown
policeChaserActivationTime = 1800 + Math.random() * 1800; // 30-60 seconds
}
// Make police chaser fall behind by reducing its speed and moving it back
tween(policeChaser, {
y: policeChaser.y + 1000 // Move it further behind the player
}, {
duration: 4000,
// Slower transition over 4 seconds
easing: tween.easeInOut,
onFinish: function onFinish() {
// After falling behind, fade out and hide
tween(policeChaser, {
alpha: 0
}, {
duration: 1500,
// Longer fade out
easing: tween.easeInOut,
onFinish: function onFinish() {
policeChaser.visible = false;
// Reset position completely off-screen to prevent any issues
policeChaser.y = player.y + 1500;
}
});
}
});
console.log('Police chaser deactivated. Next activation in', Math.floor(policeChaserActivationTime / 60), 'seconds');
}
}
// Calculate current base speed with gradual increase and cycle boost
var currentBaseSpeed = Math.min(maxSpeed, baseGameSpeed + gameTimer * speedIncrement + cycleSpeedBoost);
// Apply night speed multiplier
gameSpeed = isDay ? currentBaseSpeed : currentBaseSpeed * nightSpeedMultiplier;
// Distance tracking removed - only counting coins for score
// Spawn obstacles - optimized spawn rates for better gameplay flow
obstacleSpawnTimer++;
var obstacleSpawnRate = isDay ? 150 + Math.random() * 60 : 120 + Math.random() * 90; // More balanced spawn rates
if (obstacleSpawnTimer > obstacleSpawnRate) {
spawnObstacle();
obstacleSpawnTimer = 0;
}
// Spawn coins - optimized spawn rate for better balance
coinSpawnTimer++;
var coinSpawnRate = Math.max(40, 100 - (gameSpeed - 8) * 3); // Balanced coin spawning
if (coinSpawnTimer > coinSpawnRate + Math.random() * 30) {
spawnCoin();
coinSpawnTimer = 0;
}
// Spawn powerups (rare spawn - every 1 minute)
powerupSpawnTimer++;
if (powerupSpawnTimer > 3600) {
// 1 minute (60 seconds * 60 fps)
spawnPowerUp();
powerupSpawnTimer = 0;
}
// Spawn heart powerups (every 3 minutes)
heartPowerupSpawnTimer++;
if (heartPowerupSpawnTimer > 10800) {
// 3 minutes (180 seconds * 60 fps)
spawnHeartPowerUp();
heartPowerupSpawnTimer = 0;
}
// Spawn moving barriers (every 45 seconds)
movingBarrierSpawnTimer++;
if (movingBarrierSpawnTimer > 2700) {
// 45 seconds (45 * 60 fps)
spawnMovingBarrier();
movingBarrierSpawnTimer = 0;
}
// Spawn lane lines
laneLineSpawnTimer++;
if (laneLineSpawnTimer > 30) {
spawnLaneLine();
laneLineSpawnTimer = 0;
}
// Spawn trees
treeSpawnTimer++;
if (treeSpawnTimer > 45) {
spawnTrees();
treeSpawnTimer = 0;
}
// Update obstacles
var currentPoliceOnScreen = false;
for (var i = obstacles.length - 1; i >= 0; i--) {
var obstacle = obstacles[i];
obstacle.speed = gameSpeed;
// Check if police car is on screen
if (obstacle.y >= -200 && obstacle.y <= 2900) {
currentPoliceOnScreen = true;
}
// Initialize fade tracking if needed
if (obstacle.lastY === undefined) obstacle.lastY = obstacle.y;
if (obstacle.isFadingOut === undefined) obstacle.isFadingOut = false;
// Check if obstacle just crossed the fade threshold
if (!obstacle.isFadingOut && obstacle.lastY <= 2800 && obstacle.y > 2800) {
obstacle.isFadingOut = true;
// Start fade out animation
tween(obstacle, {
alpha: 0,
scaleX: obstacle.scaleX * 0.7,
scaleY: obstacle.scaleY * 0.7
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
obstacle.destroy();
// Remove from obstacles array after fade completes
var index = obstacles.indexOf(obstacle);
if (index > -1) {
obstacles.splice(index, 1);
}
}
});
}
// Update last position
obstacle.lastY = obstacle.y;
// Skip further processing if already fading or destroyed
if (obstacle.isFadingOut || obstacle.y > 3500) {
continue;
}
// Check collision with player (skip if obstacle is already hit)
if (!obstacle.isHit && player.intersects(obstacle)) {
// Play crash sound
try {
LK.getSound('crash').play();
} catch (e) {
console.log('Crash sound error:', e);
}
// Reduce lives
lives--;
// Update health bar display - hide the lost heart
if (lives >= 0 && lives < maxLives) {
var lostHeart = healthBars[lives]; // Use current lives as index (lives already decremented)
if (lostHeart) {
// Animate heart disappearing with red flash
tween(lostHeart, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5,
tint: 0xff0000
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
lostHeart.visible = false;
}
});
}
}
// Flash screen and remove obstacle
LK.effects.flashScreen(0xff0000, 500);
// Mark obstacle as hit to prevent multiple collisions
obstacle.isHit = true;
// Check if game over
if (lives <= 0) {
// Add delay to allow crash sound to play before game over
LK.setTimeout(function () {
LK.showGameOver();
}, 800); // Wait 800ms for crash sound to play
return;
}
// Make player temporarily invulnerable
player.alpha = 0.5;
LK.setTimeout(function () {
player.alpha = 1.0;
}, 1500); // 1.5 seconds of invulnerability
continue;
}
}
// Handle police music based on police presence
if (currentPoliceOnScreen && !policeMusicPlaying) {
// Start police siren as a sound effect that loops instead of music
// This allows it to play alongside the background music
try {
var policeSiren = LK.getSound('policemusic');
if (policeSiren) {
policeSiren.loop = true;
policeSiren.play();
}
} catch (e) {
console.log('Police siren play error:', e);
}
policeMusicPlaying = true;
policeOnScreen = true;
} else if (!currentPoliceOnScreen && policeMusicPlaying) {
// Stop police siren sound
try {
var policeSiren = LK.getSound('policemusic');
if (policeSiren) {
policeSiren.stop();
}
} catch (e) {
console.log('Police siren stop error:', e);
}
policeMusicPlaying = false;
policeOnScreen = false;
} else if (currentPoliceOnScreen && policeMusicPlaying) {
// Ensure police siren continues playing if it stopped
try {
var policeSiren = LK.getSound('policemusic');
if (policeSiren && !policeSiren.playing) {
policeSiren.loop = true;
policeSiren.play();
}
} catch (e) {
console.log('Police siren restart error:', e);
}
}
// Update coins
for (var j = coins.length - 1; j >= 0; j--) {
var coin = coins[j];
coin.speed = gameSpeed;
// Remove coins that are off screen
if (coin.y > 2800) {
coin.destroy();
coins.splice(j, 1);
continue;
}
// Check collection
if (!coin.collected && player.intersects(coin)) {
coin.collected = true;
LK.setScore(LK.getScore() + 1); // Each coin is worth exactly 1 point
scoreText.setText('Score: ' + LK.getScore());
LK.getSound('collect').play();
// Enhanced visual effect with faster animation for higher speeds
var animationDuration = Math.max(200, 400 - (gameSpeed - 8) * 10);
tween(coin, {
alpha: 0,
scaleX: 2.5,
scaleY: 2.5,
y: coin.y - 50
}, {
duration: animationDuration,
easing: tween.easeOut,
onFinish: function onFinish() {
coin.destroy();
}
});
coins.splice(j, 1);
}
}
// Update powerups
for (var p = powerups.length - 1; p >= 0; p--) {
var powerup = powerups[p];
powerup.speed = gameSpeed;
// Remove powerups that are off screen or expired
if (powerup.y > 2800 || powerup.lifetime <= 0) {
powerup.destroy();
powerups.splice(p, 1);
continue;
}
// Check collection
if (!powerup.collected && player.intersects(powerup)) {
powerup.collected = true;
hasWeapon = true;
weaponTimer = weaponDuration; // 20 seconds
LK.getSound('collect').play();
// Visual effect
tween(powerup, {
alpha: 0,
scaleX: 3,
scaleY: 3
}, {
duration: 400,
onFinish: function onFinish() {
powerup.destroy();
}
});
powerups.splice(p, 1);
}
}
// Update heart powerups
for (var hp = heartPowerups.length - 1; hp >= 0; hp--) {
var heartPowerup = heartPowerups[hp];
heartPowerup.speed = gameSpeed;
// Remove heart powerups that are off screen or expired
if (heartPowerup.y > 2800 || heartPowerup.lifetime <= 0) {
heartPowerup.destroy();
heartPowerups.splice(hp, 1);
continue;
}
// Check collection
if (!heartPowerup.collected && player.intersects(heartPowerup)) {
heartPowerup.collected = true;
// Add one life if not at maximum
if (lives < maxLives) {
lives++;
// Show the health bar corresponding to gained life with animation
if (lives > 0 && lives <= maxLives) {
var restoredHeart = healthBars[lives - 1]; // Use lives-1 as index (lives already incremented)
if (restoredHeart) {
restoredHeart.visible = true;
restoredHeart.alpha = 0;
restoredHeart.scaleX = 2.0;
restoredHeart.scaleY = 2.0;
restoredHeart.tint = 0x00ff00; // Green flash
// Animate heart appearing
tween(restoredHeart, {
alpha: 0.9,
scaleX: 1.0,
scaleY: 1.0,
tint: 0xffffff
}, {
duration: 600,
easing: tween.easeOut
});
}
}
}
LK.getSound('collect').play();
// Visual effect with heart animation
tween(heartPowerup, {
alpha: 0,
scaleX: 2.5,
scaleY: 2.5,
y: heartPowerup.y - 100
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
heartPowerup.destroy();
}
});
heartPowerups.splice(hp, 1);
}
}
// Update lane lines
for (var k = laneLines.length - 1; k >= 0; k--) {
var laneLine = laneLines[k];
laneLine.speed = gameSpeed;
// Remove lane lines that are off screen
if (laneLine.y > 2800) {
laneLine.destroy();
laneLines.splice(k, 1);
}
}
// Update trees
for (var t = trees.length - 1; t >= 0; t--) {
var tree = trees[t];
tree.speed = gameSpeed;
// Remove trees that are off screen - increased threshold to prevent immediate disappearing
if (tree.y > 3200) {
tree.destroy();
trees.splice(t, 1);
}
}
// Update trees2
for (var t2 = trees2.length - 1; t2 >= 0; t2--) {
var tree2 = trees2[t2];
tree2.speed = gameSpeed;
// Remove trees2 that are off screen - increased threshold to prevent immediate disappearing
if (tree2.y > 3200) {
tree2.destroy();
trees2.splice(t2, 1);
}
}
// Update flowers
for (var fl = flowers.length - 1; fl >= 0; fl--) {
var flower = flowers[fl];
flower.speed = gameSpeed;
// Remove flowers that are off screen
if (flower.y > 3200) {
flower.destroy();
flowers.splice(fl, 1);
}
}
// Handle rain cycle logic
if (!isRaining) {
// Check if it's time to start raining
nextRainEvent++;
if (nextRainEvent >= rainCooldown) {
// Start rain cycle
isRaining = true;
rainTimer = 0;
// Random rain duration: 10-60 seconds (600-3600 frames at 60fps)
rainDuration = 600 + Math.random() * 3000; // 10 seconds to 1 minute
nextRainEvent = 0;
// Start rain music
if (!rainSoundPlaying) {
try {
LK.playMusic('rain');
} catch (e) {
console.log('Rain music play error:', e);
}
rainSoundPlaying = true;
}
// Initialize thunder timing for this rain period
thunderTimer = 0;
nextThunderTime = 180 + Math.random() * 600; // First thunder in 3-13 seconds
console.log('Rain started for', Math.floor(rainDuration / 60), 'seconds');
}
} else {
// Currently raining
rainTimer++;
// Spawn rain particles while raining - optimized frequency for performance
rainSpawnTimer++;
if (rainSpawnTimer >= 6) {
// Spawn rain every 6 frames for balanced performance
spawnRain();
rainSpawnTimer = 0;
}
// Handle thunder during rain
thunderTimer++;
if (thunderTimer >= nextThunderTime) {
// Play thunder sound
try {
var thunderSound = LK.getSound('thunder');
if (thunderSound) {
thunderSound.play();
}
} catch (e) {
console.log('Thunder sound play error:', e);
}
// Add thunder flash effect - bright white flash
LK.effects.flashScreen(0xffffff, 300); // White flash for 300ms
// Set next thunder time (random between 5-20 seconds)
nextThunderTime = thunderTimer + (300 + Math.random() * 900); // 5-20 seconds at 60fps
}
// Check if rain should stop
if (rainTimer >= rainDuration) {
// Stop rain cycle
isRaining = false;
rainTimer = 0;
// Random cooldown period: 30-180 seconds (1800-10800 frames at 60fps)
rainCooldown = 1800 + Math.random() * 9000; // 30 seconds to 3 minutes
nextRainEvent = 0;
// Reset thunder timer
thunderTimer = 0;
nextThunderTime = 0;
// Gradually fade out existing rain drops
for (var fadeRain = 0; fadeRain < rainDrops.length; fadeRain++) {
var rainDrop = rainDrops[fadeRain];
tween(rainDrop, {
alpha: 0,
speed: rainDrop.speed * 0.3
}, {
duration: 2000 + Math.random() * 1000,
easing: tween.easeOut
});
}
console.log('Rain stopped. Next rain in', Math.floor(rainCooldown / 60), 'seconds');
}
}
// Stop rain music only when all rain effects are complete
if (!isRaining && rainSoundPlaying && rainDrops.length === 0) {
try {
LK.stopMusic();
// Restart background music after rain stops
LK.playMusic('bgmusic');
} catch (e) {
console.log('Rain music stop error:', e);
}
rainSoundPlaying = false;
}
// Spawn exhaust particles - optimized frequency for performance
exhaustSpawnTimer++;
if (exhaustSpawnTimer >= 12) {
spawnExhaustParticle();
exhaustSpawnTimer = 0;
}
// Update exhaust particles
for (var e = exhaustParticles.length - 1; e >= 0; e--) {
var particle = exhaustParticles[e];
// Remove particles that are expired or off screen
if (particle.lifetime <= 0 || particle.y < -50) {
particle.destroy();
exhaustParticles.splice(e, 1);
}
}
// Update flame particles
for (var f = flames.length - 1; f >= 0; f--) {
var flame = flames[f];
// Remove flames that are expired or off screen
if (flame.lifetime <= 0 || flame.y < -50) {
flame.destroy();
flames.splice(f, 1);
}
}
// Update smoke particles
for (var s = smokes.length - 1; s >= 0; s--) {
var smoke = smokes[s];
// Remove smoke that is expired or off screen
if (smoke.lifetime <= 0 || smoke.y < -50) {
smoke.destroy();
smokes.splice(s, 1);
}
}
// Update debris particles
for (var d = debrisParticles.length - 1; d >= 0; d--) {
var debris = debrisParticles[d];
// Remove debris that is expired or off screen
if (debris.lifetime <= 0 || debris.y > 2800) {
debris.destroy();
debrisParticles.splice(d, 1);
}
}
// Update rain particles
for (var r = rainDrops.length - 1; r >= 0; r--) {
var rain = rainDrops[r];
// Remove rain drops that are off screen
if (rain.y > 2800 || rain.x < -50 || rain.x > 2098) {
rain.destroy();
rainDrops.splice(r, 1);
}
}
// Update moving barriers
for (var mb = movingBarriers.length - 1; mb >= 0; mb--) {
var barrier = movingBarriers[mb];
barrier.speed = gameSpeed;
// Initialize fade tracking if needed
if (barrier.lastY === undefined) barrier.lastY = barrier.y;
if (barrier.isFadingOut === undefined) barrier.isFadingOut = false;
// Check if barrier just crossed the fade threshold
if (!barrier.isFadingOut && barrier.lastY <= 2800 && barrier.y > 2800) {
barrier.isFadingOut = true;
// Start fade out animation
tween(barrier, {
alpha: 0,
scaleX: barrier.scaleX * 0.7,
scaleY: barrier.scaleY * 0.7
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
barrier.destroy();
// Remove from movingBarriers array after fade completes
var index = movingBarriers.indexOf(barrier);
if (index > -1) {
movingBarriers.splice(index, 1);
}
}
});
}
// Update last position
barrier.lastY = barrier.y;
// Skip further processing if already fading or destroyed
if (barrier.isFadingOut || barrier.y > 3500) {
continue;
}
// Check collision with player (skip if barrier is already hit)
if (!barrier.isHit && player.intersects(barrier)) {
// Play crash sound
try {
LK.getSound('crash').play();
} catch (e) {
console.log('Crash sound error:', e);
}
// Reduce lives
lives--;
// Update health bar display - hide the lost heart
if (lives >= 0 && lives < maxLives) {
var lostHeart = healthBars[lives]; // Use current lives as index (lives already decremented)
if (lostHeart) {
// Animate heart disappearing with red flash
tween(lostHeart, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5,
tint: 0xff0000
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
lostHeart.visible = false;
}
});
}
}
// Flash screen and mark barrier as hit
LK.effects.flashScreen(0xff0000, 500);
barrier.isHit = true;
// Check if game over
if (lives <= 0) {
LK.setTimeout(function () {
LK.showGameOver();
}, 800);
return;
}
// Make player temporarily invulnerable
player.alpha = 0.5;
LK.setTimeout(function () {
player.alpha = 1.0;
}, 1500);
continue;
}
// Check bullet collision with moving barriers
for (var bb = bullets.length - 1; bb >= 0; bb--) {
var bullet = bullets[bb];
if (bullet && !barrier.isHit && bullet.intersects(barrier)) {
// Play police hit sound
try {
LK.getSound('policeHit').play();
} catch (e) {
console.log('Police hit sound error:', e);
}
// Destroy bullet immediately
bullet.destroy();
bullets.splice(bb, 1);
// Mark barrier as hit
barrier.isHit = true;
// Create flame effect at barrier position
createFlameEffect(barrier.x, barrier.y - 50, barrier);
// Create gold coins flying out when barrier is destroyed (optimized amount)
for (var goldCount = 0; goldCount < 5; goldCount++) {
var goldCoin = new Coin();
goldCoin.x = barrier.x + (Math.random() - 0.5) * 100;
goldCoin.y = barrier.y + (Math.random() - 0.5) * 80;
goldCoin.scaleX = 0.8 + Math.random() * 0.4;
goldCoin.scaleY = 0.8 + Math.random() * 0.4;
goldCoin.collected = false;
goldCoin.speed = gameSpeed * 0.5;
goldCoin.tint = 0xFFD700;
var randomX = goldCoin.x + (Math.random() - 0.5) * 200;
var randomY = goldCoin.y + Math.random() * 150 + 50;
tween(goldCoin, {
x: randomX,
y: randomY,
rotation: Math.PI * 4
}, {
duration: 800 + Math.random() * 400,
easing: tween.easeOut
});
coins.push(goldCoin);
game.addChild(goldCoin);
}
// Create burning effect
tween(barrier, {
tint: 0x000000,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
barrier.isDestroyed = true;
barrier.destroy();
var index = movingBarriers.indexOf(barrier);
if (index > -1) {
movingBarriers.splice(index, 1);
}
}
});
break;
}
}
}
// Handle weapon system
if (hasWeapon) {
weaponTimer--;
if (weaponTimer <= 0) {
hasWeapon = false;
}
// Shoot bullets every 15 frames for balanced firing rate
if (LK.ticks % 15 === 0) {
var bullet = new Bullet();
bullet.x = player.x;
bullet.y = player.y - 100;
bullets.push(bullet);
game.addChild(bullet);
// Play gun sound with reduced volume for less audio clutter
try {
var gunSound = LK.getSound('gunshot');
gunSound.volume = 0.2;
gunSound.play();
} catch (e) {
console.log('Gun sound error:', e);
}
}
}
// Update bullets
for (var b = bullets.length - 1; b >= 0; b--) {
var bullet = bullets[b];
// Remove bullets that are off screen
if (bullet.y < -50) {
bullet.destroy();
bullets.splice(b, 1);
continue;
}
// Check bullet collision with obstacles
for (var o = obstacles.length - 1; o >= 0; o--) {
var obstacle = obstacles[o];
if (!obstacle.isHit && bullet.intersects(obstacle)) {
// Play police hit sound
try {
LK.getSound('policeHit').play();
} catch (e) {
console.log('Police hit sound error:', e);
}
// Destroy bullet immediately
bullet.destroy();
bullets.splice(b, 1);
// Mark obstacle as hit to prevent further collisions
obstacle.isHit = true;
// Create flame effect at police car position with car reference
createFlameEffect(obstacle.x, obstacle.y - 50, obstacle);
// Create 6 gold coins flying out when police car is destroyed (optimized amount)
for (var goldCount = 0; goldCount < 6; goldCount++) {
var goldCoin = new Coin();
goldCoin.x = obstacle.x + (Math.random() - 0.5) * 100;
goldCoin.y = obstacle.y + (Math.random() - 0.5) * 80;
goldCoin.scaleX = 0.8 + Math.random() * 0.4;
goldCoin.scaleY = 0.8 + Math.random() * 0.4;
goldCoin.collected = false;
goldCoin.speed = gameSpeed * 0.5; // Slower than normal coins
// Make coins golden colored
goldCoin.tint = 0xFFD700;
// Add random movement with tween animation
var randomX = goldCoin.x + (Math.random() - 0.5) * 200;
var randomY = goldCoin.y + Math.random() * 150 + 50;
tween(goldCoin, {
x: randomX,
y: randomY,
rotation: Math.PI * 4
}, {
duration: 800 + Math.random() * 400,
easing: tween.easeOut
});
coins.push(goldCoin);
game.addChild(goldCoin);
}
// Create burning effect with flames
tween(obstacle, {
tint: 0x000000,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
obstacle.isDestroyed = true; // Mark as destroyed for flame cleanup
obstacle.destroy();
// Remove from obstacles array after animation completes
var index = obstacles.indexOf(obstacle);
if (index > -1) {
obstacles.splice(index, 1);
}
}
});
break;
}
}
}
// Update day/night cycle
dayNightTimer++;
if (dayNightTimer >= dayNightCycleDuration) {
transitionDayNight();
dayNightTimer = 0;
}
// Police exhaust particles now stay in their natural layer order
// Road color no longer changes automatically
};
// Road color change function (kept for potential future use)
function changeRoadColor(newColor) {
roadBackground.tint = newColor;
}
// Day/night cycle transition function
function transitionDayNight() {
if (isTransitioning) return;
isTransitioning = true;
isDay = !isDay;
var targetColors = isDay ? dayColors : nightColors;
var transitionDuration = 2000; // 2 seconds transition
// Increment cycle count and apply speed boost when transitioning to day (completing full cycle)
if (isDay) {
cycleCount++;
cycleSpeedBoost = cycleCount * speedBoostPerCycle;
}
// Immediately update game speed for the new cycle
var currentBaseSpeed = Math.min(maxSpeed, baseGameSpeed + gameTimer * speedIncrement + cycleSpeedBoost);
gameSpeed = isDay ? currentBaseSpeed : currentBaseSpeed * nightSpeedMultiplier;
// Transition background color
tween(game, {
backgroundColor: targetColors.sky
}, {
duration: transitionDuration,
easing: tween.easeInOut
});
// Transition forest backgrounds
tween(leftForest, {
tint: targetColors.forest
}, {
duration: transitionDuration,
easing: tween.easeInOut
});
tween(rightForest, {
tint: targetColors.forest
}, {
duration: transitionDuration,
easing: tween.easeInOut
});
// Transition road background
tween(roadBackground, {
tint: targetColors.road
}, {
duration: transitionDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
isTransitioning = false;
}
});
// Adjust trees brightness for night/day
var treeBrightness = isDay ? 1.0 : 0.6;
for (var i = 0; i < trees.length; i++) {
var tree = trees[i];
tween(tree, {
alpha: treeBrightness
}, {
duration: transitionDuration,
easing: tween.easeInOut
});
}
// Adjust trees2 brightness for night/day
for (var i2 = 0; i2 < trees2.length; i2++) {
var tree2 = trees2[i2];
tween(tree2, {
alpha: treeBrightness
}, {
duration: transitionDuration,
easing: tween.easeInOut
});
}
// Adjust flowers brightness for night/day
var flowerBrightness = isDay ? 1.0 : 0.5;
for (var f = 0; f < flowers.length; f++) {
var flower = flowers[f];
tween(flower, {
alpha: flowerBrightness
}, {
duration: transitionDuration,
easing: tween.easeInOut
});
}
// Control player headlights based on day/night
var headlightAlpha = isDay ? 0 : 0.8;
tween(player.leftHeadlight, {
alpha: headlightAlpha
}, {
duration: transitionDuration,
easing: tween.easeInOut
});
tween(player.rightHeadlight, {
alpha: headlightAlpha
}, {
duration: transitionDuration,
easing: tween.easeInOut
});
}
// Spawn initial powerup at game start
spawnPowerUp();
// Initialize first rain cycle
rainCooldown = 1800 + Math.random() * 9000; // 30 seconds to 3 minutes before first rain
nextRainEvent = 0;
// Start background music - this will be the base music layer
LK.playMusic('bgmusic', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
;
Polis arabası kuş bakışı. In-Game asset. 2d. High contrast. No shadows
Düz gri renk. In-Game asset. 2d. High contrast. No shadows
Kuş bakışı agaç. In-Game asset. 2d. High contrast. No shadows
Kalp 3d. In-Game asset. 2d. High contrast. No shadows
Kırmızı ve sarı gradient renk. In-Game asset. 2d. High contrast. No shadows
Çiçek kuş bakışı. In-Game asset. 2d. High contrast. No shadows
Kuş bakışı polis aracı. In-Game asset. 2d. High contrast. No shadows