/****
* 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