/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
/**
* Bonus Asset - Extensible Power-Up System
*
* The Bonus asset supports a wide range of features and properties to enable rich gameplay and power-up mechanics.
* Below are some of the key properties and extensibility points for the Bonus asset:
*
* 1. duration: (Number) - Duration of the bonus effect in frames or seconds.
* Example: duration: 300 // lasts for 5 seconds at 60fps
*
* 2. stackable: (Boolean) - Whether the bonus effect can be stacked or only the duration is extended.
* Example: stackable: true
*
* 3. isActive: (Boolean) - Indicates if the bonus is currently active.
* Example: isActive: true
*
* 4. effect: (String) - Visual effect or animation type when the bonus is picked up or active.
* Example: effect: 'glow', effect: 'pulse'
*
* 5. scoreMultiplier: (Number) - Multiplier applied to score when the bonus is active.
* Example: scoreMultiplier: 2
*
* 6. target: (String) - The target of the bonus effect (e.g., 'player', 'allEnemies').
* Example: target: 'player'
*
* 7. onActivate: (Function) - Custom behavior function triggered when the bonus is activated.
* Example: onActivate: function () { /* custom logic *\/ }
*
* 8. Visual Properties: (Object) - Customization for appearance such as icon, color, animation speed.
* Example: icon: 'star', color: 0xff00ff
*
* 9. usageLimit: (Number) - How many times the bonus can be used.
* Example: usageLimit: 3
*
* 10. cooldown: (Number) - Cooldown period before the bonus can be picked up again.
* Example: cooldown: 600 // in frames
*
* 11. type: (String) - The type of bonus (e.g., 'speed', 'shield', 'invisible', 'score').
* Example: type: 'shield'
*
* 12. sound: (String) - Sound effect to play when the bonus is picked up or active.
* Example: sound: 'bonus_pickup'
*
* 13. spawnCondition: (String) - Condition for spawning the bonus.
* Example: spawnCondition: 'score>1000'
*
* 14. area: (Object) - Area of effect for the bonus.
* Example: area: { x: 100, y: 200, radius: 300 }
*
* 15. isNegative: (Boolean) - Whether the bonus is a negative (penalty) effect.
* Example: isNegative: true
*
* The Bonus class can be extended or configured with these properties to create a variety of power-ups,
* such as temporary shields, score multipliers, speed boosts, area effects, and more.
* This system allows for both positive and negative bonuses, visual and audio feedback, and custom behaviors.
*/
// Bonus Item Class
var Bonus = Container.expand(function () {
var self = Container.call(this);
// 50 simple bonus types, all using the same asset but different type strings
var types = [];
for (var i = 1; i <= 40; i++) {
types.push({
id: 'bonus',
type: 'score' + i
});
}
// Add new fun bonus types!
types.push({
id: 'bonus',
type: 'shield'
});
types.push({
id: 'bonus',
type: 'double'
});
types.push({
id: 'bonus',
type: 'slowmo'
}); // Slow motion for a short time
types.push({
id: 'bonus',
type: 'shrink'
}); // Shrink player for a short time
types.push({
id: 'bonus',
type: 'invincible'
}); // Invincible for a short time
types.push({
id: 'bonus',
type: 'teleport'
}); // Teleport player to random lane
types.push({
id: 'bonus',
type: 'clear'
}); // Clear all obstacles/enemies
types.push({
id: 'bonus',
type: 'magnet'
}); // Attract bonuses for a short time
types.push({
id: 'bonus',
type: 'reverse'
}); // Reverse enemy direction for a short time
types.push({
id: 'bonus',
type: 'score'
}); // fallback classic
var chosen = types[Math.floor(Math.random() * types.length)];
self.type = chosen.type;
// Use different tints for power-ups
var tint = 0xffffff;
if (self.type === 'shield') tint = 0x42a5f5;else if (self.type === 'double') tint = 0xffd600;else {
// Give each score bonus a different color for fun
// Cycle through a palette
var palette = [0xffffff, 0xffe082, 0x80cbc4, 0xffab91, 0xb39ddb, 0xa5d6a7, 0xffccbc, 0x90caf9, 0xf48fb1, 0xc5e1a5, 0xffecb3, 0xb0bec5, 0xd7ccc8, 0xffb74d, 0x81d4fa, 0xe1bee7, 0xc8e6c9, 0xff8a65, 0x9575cd, 0x4db6ac, 0x7986cb, 0x64b5f6, 0xffd54f, 0x4fc3f7, 0x81c784, 0xba68c8, 0x4dd0e1, 0xf06292, 0xa1887f, 0x90a4ae, 0x388e3c, 0xdce775, 0x00bcd4, 0x8d6e63, 0x43a047, 0x00acc1, 0x689f38, 0x039be5, 0x7e57c2, 0x0288d1, 0x0097a7, 0x388e3c, 0x1976d2, 0x0288d1, 0x009688, 0x43a047, 0x689f38];
// Extract number from type string, e.g. "score17" -> 17
var idx = 0;
if (self.type.indexOf('score') === 0 && self.type.length > 5) {
idx = parseInt(self.type.substring(5), 10) - 1;
if (isNaN(idx) || idx < 0) idx = 0;
}
tint = palette[idx % palette.length];
}
var b = self.attachAsset(chosen.id, {
anchorX: 0.5,
anchorY: 0.5
});
b.tint = tint;
self.width = b.width;
self.height = b.height;
self.speed = 12 + Math.random() * 5; // Slower base speed for easier gameplay
self.lane = 0;
self.update = function () {
self.y += self.speed;
};
self.getBounds = function () {
// Shrink hitbox for better gameplay feel
var w = b.width * 0.7;
var h = b.height * 0.7;
return {
x: self.x - w / 2,
y: self.y - h / 2,
width: w,
height: h
};
};
return self;
});
// Enemy Car Class
var EnemyCar = Container.expand(function () {
var self = Container.call(this);
// Randomly select one of the enemy car asset ids
var enemyCarIds = ['enemyCar', 'enemyCar2', 'enemyCar3', 'enemyCar4'];
var chosenId = enemyCarIds[Math.floor(Math.random() * enemyCarIds.length)];
var car = self.attachAsset(chosenId, {
anchorX: 0.5,
anchorY: 0.5
});
self.width = car.width;
self.height = car.height;
self.speed = 12 + Math.random() * 5; // Slower base speed for easier gameplay
self.lane = 0;
self.update = function () {
// Store lastX and lastY for event triggers and lane change logic
if (typeof self.lastX === "undefined") self.lastX = self.x;
if (typeof self.lastY === "undefined") self.lastY = self.y;
// Before moving, check if moving would cause overlap with any obstacle or enemy car in the same lane
var nextY = self.y + self.speed;
var canMove = true;
// Check with obstacles in same lane
for (var i = 0; i < obstacles.length; i++) {
var obs = obstacles[i];
if (obs.lane === self.lane) {
var myBounds = {
x: self.x - self.width * 0.35,
y: nextY - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var obsBounds = obs.getBounds();
if (!(myBounds.x + myBounds.width < obsBounds.x || myBounds.x > obsBounds.x + obsBounds.width || myBounds.y + myBounds.height < obsBounds.y || myBounds.y > obsBounds.y + obsBounds.height)) {
canMove = false;
break;
}
}
}
// Check with other enemy cars in same lane
if (canMove) {
for (var i = 0; i < enemyCars.length; i++) {
var other = enemyCars[i];
if (other !== self && other.lane === self.lane) {
var myBounds = {
x: self.x - self.width * 0.35,
y: nextY - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var otherBounds = other.getBounds();
if (!(myBounds.x + myBounds.width < otherBounds.x || myBounds.x > otherBounds.x + otherBounds.width || myBounds.y + myBounds.height < otherBounds.y || myBounds.y > otherBounds.y + otherBounds.height)) {
canMove = false;
break;
}
}
}
}
if (canMove) {
self.y = nextY;
}
// Randomly decide to change lane (only if not already changing, and not too often)
// Each enemy car can only change lane once, and only by one lane left or right
if (typeof self._hasChangedLane === "undefined") self._hasChangedLane = false;
if (!self._laneChangeCooldown) self._laneChangeCooldown = 0;
if (!self._isChangingLane) self._isChangingLane = false;
// Only allow lane change if not currently changing, not near the bottom, and not already changed lane
// --- Prevent lane change if there is a bonus or hearth in the current lane and close in Y ---
var blockLaneChange = false;
if (typeof bonuses !== "undefined") {
for (var i = 0; i < bonuses.length; i++) {
var bonus = bonuses[i];
if (bonus.lane === self.lane && Math.abs(bonus.y - self.y) < self.height * 1.2) {
blockLaneChange = true;
break;
}
}
}
if (!blockLaneChange && typeof hearthBonuses !== "undefined") {
for (var i = 0; i < hearthBonuses.length; i++) {
var hearth = hearthBonuses[i];
if (hearth.lane === self.lane && Math.abs(hearth.y - self.y) < self.height * 1.2) {
blockLaneChange = true;
break;
}
}
}
if (!self._isChangingLane && self._laneChangeCooldown <= 0 && self.y > 0 && self.y < 2200 && !self._hasChangedLane && !blockLaneChange) {
// Add probability for lane change: 1% chance (further reduced from 3%)
var laneChangeProbability = 0.01;
if (Math.random() < laneChangeProbability) {
// Only allow to change to adjacent lane (left or right by 1)
var availableLanes = [];
for (var offset = -1; offset <= 1; offset += 2) {
var testLane = self.lane + offset;
if (testLane < 0 || testLane >= laneCenters.length) continue;
var canChange = true;
// Check for other enemy cars in the target lane, close in Y
for (var i = 0; i < enemyCars.length; i++) {
var other = enemyCars[i];
if (other !== self && other.lane === testLane && Math.abs(other.y - self.y) < self.height * 1.2) {
canChange = false;
break;
}
}
// Check for obstacles in the target lane, close in Y
if (canChange) {
for (var i = 0; i < obstacles.length; i++) {
var obs = obstacles[i];
if (obs.lane === testLane && Math.abs(obs.y - self.y) < self.height * 1.2) {
canChange = false;
break;
}
}
}
// Check for bonuses in the target lane, close in Y (prevent lane change if would overlap a bonus)
if (canChange && typeof bonuses !== "undefined") {
for (var i = 0; i < bonuses.length; i++) {
var bonus = bonuses[i];
if (bonus.lane === testLane) {
// Predict where self would be after lane change (same y, new lane)
var myBounds = {
x: laneCenters[testLane] - self.width * 0.35,
y: self.y - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var bonusBounds = bonus.getBounds();
if (!(myBounds.x + myBounds.width < bonusBounds.x || myBounds.x > bonusBounds.x + bonusBounds.width || myBounds.y + myBounds.height < bonusBounds.y || myBounds.y > bonusBounds.y + bonusBounds.height)) {
canChange = false;
break;
}
}
}
}
// Check for hearth bonuses in the target lane, close in Y (prevent lane change if would overlap a hearth bonus)
if (canChange && typeof hearthBonuses !== "undefined") {
for (var i = 0; i < hearthBonuses.length; i++) {
var hearth = hearthBonuses[i];
if (hearth.lane === testLane) {
// Predict where self would be after lane change (same y, new lane)
var myBounds = {
x: laneCenters[testLane] - self.width * 0.35,
y: self.y - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var hearthBounds = hearth.getBounds();
// Prevent lane change if would overlap with hearth bonus
if (!(myBounds.x + myBounds.width < hearthBounds.x || myBounds.x > hearthBounds.x + hearthBounds.width || myBounds.y + myBounds.height < hearthBounds.y || myBounds.y > hearthBounds.y + hearthBounds.height)) {
canChange = false;
break;
}
}
}
}
// Prevent two cars from moving into the same lane and overlapping during lane change
if (canChange) {
for (var i = 0; i < enemyCars.length; i++) {
var other = enemyCars[i];
if (other !== self && other._isChangingLane && other._targetLane === testLane) {
// Predict where both cars will be during lane change
var myTargetY = self.y;
var otherTargetY = other.y;
// If Y overlap at the end of lane change, block
if (Math.abs(myTargetY - otherTargetY) < self.height * 1.2) {
canChange = false;
break;
}
}
}
}
if (canChange) {
availableLanes.push(testLane);
}
}
// If there is at least one available adjacent lane, pick one randomly and change
if (availableLanes.length > 0) {
var newLane = availableLanes[Math.floor(Math.random() * availableLanes.length)];
// Begin lane change
self._isChangingLane = true;
self._targetLane = newLane;
self._laneChangeStartX = self.x;
self._laneChangeStartY = self.y;
self._laneChangeProgress = 0;
self._laneChangeDuration = 18 + Math.floor(Math.random() * 10); // frames
self._hasChangedLane = true; // Mark as changed lane, so only once
}
}
}
// If currently changing lane, animate X toward new lane center using tween
if (self._isChangingLane) {
if (!self._laneTweenStarted) {
self._laneTweenStarted = true;
tween(self, {
x: laneCenters[self._targetLane]
}, {
duration: self._laneChangeDuration * 16,
easing: tween.easeInOutSine,
onFinish: function onFinish() {
self.x = laneCenters[self._targetLane];
self.lane = self._targetLane;
self._isChangingLane = false;
self._laneChangeCooldown = 60 + Math.floor(Math.random() * 60);
self._laneTweenStarted = false;
}
});
}
} else {
self._laneTweenStarted = false;
// Not changing lane, decrement cooldown if needed
if (self._laneChangeCooldown > 0) self._laneChangeCooldown--;
}
// Update lastX and lastY for next frame
self.lastX = self.x;
self.lastY = self.y;
};
self.getBounds = function () {
// Shrink hitbox for better gameplay feel
var w = car.width * 0.7;
var h = car.height * 0.7;
return {
x: self.x - w / 2,
y: self.y - h / 2,
width: w,
height: h
};
};
return self;
});
// Hearth Bonus Class
var HearthBonus = Container.expand(function () {
var self = Container.call(this);
// Make hearth asset medium sized and visible (no pulse)
var h = self.attachAsset('hearth', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.15,
scaleY: 1.15
});
self.width = h.width * 1.15;
self.height = h.height * 1.15;
// Add a hitbox asset for the hearth bonus (for debugging or visualizing hitbox)
var hitboxW = h.width * 1.15 * 0.7;
var hitboxH = h.height * 1.15 * 0.7;
var hitbox = self.attachAsset('lane', {
anchorX: 0.5,
anchorY: 0.5,
width: hitboxW,
height: hitboxH,
color: 0xff0000
});
hitbox.alpha = 0.18; // Make it semi-transparent for debugging
hitbox.visible = false; // Set to true if you want to see the hitbox
self.speed = 12 + Math.random() * 5; // Slower base speed for easier gameplay
self.lane = 0;
self.update = function () {
self.y += self.speed;
};
self.getBounds = function () {
// Shrink hitbox for better gameplay feel
var w = h.width * 1.15 * 0.7;
var hgt = h.height * 1.15 * 0.7;
return {
x: self.x - w / 2,
y: self.y - hgt / 2,
width: w,
height: hgt
};
};
return self;
});
// Obstacle Class
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obs = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = obs.width;
self.height = obs.height;
self.speed = 12 + Math.random() * 5; // Slower base speed for easier gameplay
self.lane = 0;
self.update = function () {
// Before moving, check if moving would cause overlap with any enemy car or obstacle in the same lane
var nextY = self.y + self.speed;
var canMove = true;
// Check with enemy cars in same lane
for (var i = 0; i < enemyCars.length; i++) {
var car = enemyCars[i];
if (car.lane === self.lane) {
var myBounds = {
x: self.x - self.width * 0.35,
y: nextY - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var carBounds = car.getBounds();
if (!(myBounds.x + myBounds.width < carBounds.x || myBounds.x > carBounds.x + carBounds.width || myBounds.y + myBounds.height < carBounds.y || myBounds.y > carBounds.y + carBounds.height)) {
canMove = false;
break;
}
}
}
// Check with other obstacles in same lane
if (canMove) {
for (var i = 0; i < obstacles.length; i++) {
var other = obstacles[i];
if (other !== self && other.lane === self.lane) {
var myBounds = {
x: self.x - self.width * 0.35,
y: nextY - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var otherBounds = other.getBounds();
if (!(myBounds.x + myBounds.width < otherBounds.x || myBounds.x > otherBounds.x + otherBounds.width || myBounds.y + myBounds.height < otherBounds.y || myBounds.y > otherBounds.y + otherBounds.height)) {
canMove = false;
break;
}
}
}
}
if (canMove) {
self.y = nextY;
}
};
self.getBounds = function () {
// Shrink hitbox for better gameplay feel
var w = obs.width * 0.7;
var h = obs.height * 0.7;
return {
x: self.x - w / 2,
y: self.y - h / 2,
width: w,
height: h
};
};
return self;
});
// Player Car Class
var PlayerCar = Container.expand(function () {
var self = Container.call(this);
var car = self.attachAsset('playerCar', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.55,
scaleY: 0.55
});
self._carAsset = car; // Store reference for shrink/restore
self.width = car.width * 0.55;
self.height = car.height * 0.55;
// For touch drag offset
self.dragOffsetX = 0;
self.dragOffsetY = 0;
// For collision
self.getBounds = function () {
// Shrink hitbox for better gameplay feel
var w = car.width * 0.55;
var h = car.height * 0.55;
return {
x: self.x - w / 2,
y: self.y - h / 2,
width: w,
height: h
};
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// Intense mode music (for hard stage, optional)
// Main background music (looped)
// --- MUSIC ---
// Win
// Game over
// Button tap (UI)
// Lane change (player moves to another lane)
// Reverse pickup
// Magnet pickup
// Clear pickup
// Teleport pickup
// Invincible pickup
// Shrink pickup
// Slowmo pickup
// Double score pickup
// Shield pickup
// Bonus pickup
// Car crash
// Car engine accelerate (short burst)
// Car engine idle (looped quietly in background)
// --- SOUND EFFECTS ---
// Tree and plant assets for background
// Road and lane setup
// Car (player)
// Enemy car
// Obstacle (barrier)
// Road lane
// Bonus item
// Hearth asset for health bar
var roadWidth = 900;
var roadLeft = (2048 - roadWidth) / 2;
var roadRight = roadLeft + roadWidth;
var laneCount = 4;
var laneWidth = roadWidth / laneCount;
var laneCenters = [];
for (var i = 0; i < laneCount; i++) {
laneCenters.push(roadLeft + laneWidth / 2 + i * laneWidth);
}
// --- Add colorful trees and plants to left and right sides of the road as background ---
// Reduce number of decorations for optimization
var bgDecor = [];
var decorTypes = [{
id: 'tree1',
yOffset: 0
}, {
id: 'tree2',
yOffset: 40
}, {
id: 'plant1',
yOffset: 120
}, {
id: 'plant2',
yOffset: 180
}, {
id: 'plant3',
yOffset: 240
}];
// Place more decorations for a richer environment (planets/trees/plants)
for (var side = 0; side < 2; side++) {
// 0: left, 1: right
for (var i = 0; i < 4; i++) {
// Increased from 2 to 4 per side
var typeIdx = i % decorTypes.length;
var decor = LK.getAsset(decorTypes[typeIdx].id, {
anchorX: 0.5,
anchorY: 0.5
});
// X position: left or right of road, with some random offset for natural look
if (side === 0) {
decor.x = roadLeft - 90 + Math.random() * 30;
} else {
decor.x = roadRight + 90 - Math.random() * 30;
}
// Y position: staggered vertically, with some random offset, spacing decreased for more planets
decor.y = 300 + i * 450 + decorTypes[typeIdx].yOffset + Math.random() * 40;
// Store for scrolling
decor._baseY = decor.y;
decor._side = side;
decor._typeIdx = typeIdx;
game.addChild(decor);
bgDecor.push(decor);
}
}
// Draw dashed lane markers for a more realistic road look
var laneMarkers = [];
var dashHeight = 120;
var dashGap = 80;
var markerWidth = 24;
var markerColor = 0xf7f3f3;
for (var i = 1; i < laneCount; i++) {
var x = roadLeft + i * laneWidth;
for (var y = -dashHeight; y < 2732 + dashHeight; y += dashHeight + dashGap) {
var marker = LK.getAsset('lane', {
anchorX: 0.5,
anchorY: 0.5,
x: x,
y: y,
width: markerWidth,
height: dashHeight,
color: markerColor
});
marker.laneIdx = i;
marker._dash = true;
game.addChild(marker);
laneMarkers.push(marker);
}
}
// Player car
var player = new PlayerCar();
player.x = laneCenters[1];
player.y = 2732 - 500;
game.addChild(player);
// --- Bonus effect indicator above player ---
var bonusIcon = new Text2('', {
size: 90,
fill: "#fff",
font: "Arial"
});
bonusIcon.anchor.set(0.5, 1);
bonusIcon.x = player.x;
bonusIcon.y = player.y - player.height / 2 - 30;
bonusIcon.alpha = 0;
game.addChild(bonusIcon);
// Score
var score = 0;
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// High score tracking and display (top left, just right of menu area)
var highScore = storage.highScore || 0;
var highScoreTxt = new Text2('HIGH SCORE: ' + highScore, {
size: 38,
fill: 0xFF0000,
font: "Arial"
});
highScoreTxt.anchor.set(0, 0);
// Place high score at x=-700 (as requested), y=0
highScoreTxt.x = -700;
highScoreTxt.y = 0;
LK.gui.top.addChild(highScoreTxt);
// Player health (lives)
var MAX_PLAYER_HEALTH = 8;
var playerHealth = MAX_PLAYER_HEALTH;
// Heart bar for health (top left, outside menu area)
var healthBarHearts = [];
function updateHealthBar() {
// Remove old hearts
for (var i = 0; i < healthBarHearts.length; i++) {
if (healthBarHearts[i].parent) healthBarHearts[i].parent.removeChild(healthBarHearts[i]);
}
healthBarHearts = [];
// Draw hearts for each health, vertically stacked
// Dynamically calculate heart size and margin to fit all hearts in available height
var availableHeight = 900; // plenty of space vertically
var maxHearts = Math.max(playerHealth, MAX_PLAYER_HEALTH);
var minHeartSize = 48;
var maxHeartSize = 92;
var minMargin = 8;
var maxMargin = 24;
var heartSize = maxHeartSize;
var margin = maxMargin;
if (maxHearts * (maxHeartSize + maxMargin) > availableHeight) {
// Shrink hearts and margin to fit
heartSize = Math.max(minHeartSize, Math.floor((availableHeight - (maxHearts - 1) * minMargin) / maxHearts));
margin = Math.max(minMargin, Math.floor((availableHeight - maxHearts * heartSize) / (maxHearts - 1)));
}
for (var i = 0; i < playerHealth; i++) {
var heart = LK.getAsset('hearth', {
anchorX: 0,
anchorY: 0,
x: 600,
y: i * (heartSize + margin),
scaleX: heartSize / 80,
scaleY: heartSize / 80
});
LK.gui.top.addChild(heart);
healthBarHearts.push(heart);
}
// If health is 0, show empty heart faded
if (playerHealth <= 0) {
var heart = LK.getAsset('hearth', {
anchorX: 0,
anchorY: 0,
x: 600,
y: 0,
scaleX: heartSize / 80,
scaleY: heartSize / 80
});
heart.alpha = 0.4;
LK.gui.top.addChild(heart);
healthBarHearts.push(heart);
}
}
updateHealthBar();
// Bonus score
var bonusScore = 0;
var bonusTxt = new Text2('', {
size: 70,
fill: 0xFFFA00 // Bright yellow for high visibility
});
bonusTxt.anchor.set(0.5, 0);
// Move the bonus text further down (e.g. 180px from the top)
bonusTxt.y = 180;
LK.gui.top.addChild(bonusTxt);
// Bonus usage count text
var bonusUsageCount = 0;
var bonusUsageTxt = new Text2('', {
size: 70,
fill: "#fff",
font: "Arial"
});
bonusUsageTxt.anchor.set(0.5, 0);
// Place below the score, but above the bonusTxt
bonusUsageTxt.y = 120;
LK.gui.top.addChild(bonusUsageTxt);
// Game state
var enemyCars = [];
var obstacles = [];
var bonuses = [];
var hearthBonuses = []; // Array for hearth bonuses
var gameSpeed = 18;
var ticksSinceStart = 0;
var lastSpawnTick = 0;
var lastBonusTick = 0;
var lastHearthBonusTick = 0; // Track hearth bonus spawn
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
var playerStartX = 0;
var playerStartY = 0;
var lastCrash = false;
// Mouse/touch drag to move player car directly with finger/mouse
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
var playerStartX = 0;
var playerStartY = 0;
game.down = function (x, y, obj) {
// Only start drag if touch/click is on the player car
// Use a generous hitbox for mobile
var bounds = player.getBounds();
if (x >= bounds.x - 60 && x <= bounds.x + bounds.width + 60 && y >= bounds.y - 60 && y <= bounds.y + bounds.height + 60) {
isDragging = true;
dragStartX = x;
dragStartY = y;
playerStartX = player.x;
playerStartY = player.y;
// Cancel any lane tween if present
if (typeof playerMoveTween !== "undefined" && playerMoveTween && playerMoveTween.cancel) playerMoveTween.cancel();
}
};
game.move = function (x, y, obj) {
if (isDragging) {
// Move player car with finger/mouse, but clamp to road
var dx = x - dragStartX;
var dy = y - dragStartY;
var newX = playerStartX + dx;
var newY = playerStartY + dy;
// Clamp X to road
var minX = roadLeft + player.width / 2;
var maxX = roadRight - player.width / 2;
if (newX < minX) newX = minX;
if (newX > maxX) newX = maxX;
// Clamp Y to visible area (bottom 2/3 of screen)
var minY = 600 + player.height / 2;
var maxY = 2732 - player.height / 2;
if (newY < minY) newY = minY;
if (newY > maxY) newY = maxY;
// Instantly move player car to finger position for direct control
player.x = newX;
player.y = newY;
}
};
game.up = function (x, y, obj) {
isDragging = false;
};
// Helper: collision check (AABB)
function intersects(a, b) {
var ab = a.getBounds();
var bb = b.getBounds();
return ab.x < bb.x + bb.width && ab.x + ab.width > bb.x && ab.y < bb.y + bb.height && ab.y + ab.height > bb.y;
}
// Main game loop
game.update = function () {
ticksSinceStart++;
// --- Score-based difficulty levels ---
// 0: Çok Kolay (0+), 1: Kolay (5000+), 2: Orta (10000+), 3: Zor (20000+), 4: Çok Zor (50000+), 5: İmkansız (100000+)
if (!game._scoreDifficultyLevel && game._scoreDifficultyLevel !== 0) game._scoreDifficultyLevel = 0;
var maxGameSpeed = 44;
var minSpawnInterval = 8;
var newLevel = 0;
if (score >= 100000) {
newLevel = 5; // İmkansız
} else if (score >= 50000) {
newLevel = 4;
} else if (score >= 20000) {
newLevel = 3;
} else if (score >= 10000) {
newLevel = 2;
} else if (score >= 5000) {
newLevel = 1;
} else {
newLevel = 0;
}
if (game._scoreDifficultyLevel !== newLevel) {
game._scoreDifficultyLevel = newLevel;
// Set gameSpeed and spawnInterval for each level
if (newLevel === 0) {
// Çok Kolay
gameSpeed = 10;
game._spawnInterval = 70;
} else if (newLevel === 1) {
// Kolay
gameSpeed = 15;
game._spawnInterval = 32;
} else if (newLevel === 2) {
// Orta
gameSpeed = 20;
game._spawnInterval = 24;
} else if (newLevel === 3) {
// Zor
gameSpeed = 25;
game._spawnInterval = 16;
} else if (newLevel === 4) {
// Çok Zor
gameSpeed = 30;
game._spawnInterval = 8;
} else if (newLevel === 5) {
// İmkansız
gameSpeed = 40;
game._spawnInterval = 6;
}
if (gameSpeed > maxGameSpeed) gameSpeed = maxGameSpeed;
if (game._spawnInterval < minSpawnInterval) game._spawnInterval = minSpawnInterval;
}
// Move background trees and plants for scrolling effect (skip if off-screen for perf)
for (var i = 0; i < bgDecor.length; i++) {
var decor = bgDecor[i];
if (decor.y > 2732 + 120) {
// If decor goes off bottom, loop to top with new random offset for variety
decor.y -= 2 * 1200; // Adjusted for reduced number of decorations
decor.y += Math.random() * 40 - 20;
// Optionally randomize X a bit for more natural look
if (decor._side === 0) {
decor.x = roadLeft - 90 + Math.random() * 30;
} else {
decor.x = roadRight + 90 - Math.random() * 30;
}
} else {
decor.y += gameSpeed;
}
}
// Move lane markers for scrolling effect (dashed road lines)
for (var i = 0; i < laneMarkers.length; i++) {
var marker = laneMarkers[i];
marker.y += gameSpeed;
// If marker goes off bottom, loop to top
if (marker.y > 2732 + marker.height / 2) {
marker.y -= 2732 + marker.height + 120; // 120 is extra buffer
}
}
// Spawn enemy cars and obstacles, ensuring no overlap with any existing object
if (ticksSinceStart - lastSpawnTick > (game._spawnInterval || 36)) {
lastSpawnTick = ticksSinceStart;
// Randomly pick lanes to spawn cars/obstacles, but always leave at least one lane empty for the player to pass
var spawnLanes = [];
for (var i = 0; i < laneCount; i++) {
if (Math.random() < 0.5) spawnLanes.push(i);
}
// Always leave at least one lane empty: if all lanes are filled, remove one at random
if (spawnLanes.length >= laneCount) {
var idxToRemove = Math.floor(Math.random() * spawnLanes.length);
spawnLanes.splice(idxToRemove, 1);
}
// If no lanes selected, force one random lane to be filled
if (spawnLanes.length == 0) spawnLanes.push(Math.floor(Math.random() * laneCount));
// Limit to max 2 obstacles per spawn (was 3)
var obstacleCountThisSpawn = 0;
// Track which lanes will have obstacles this spawn
var obstacleLanesThisSpawn = [];
// Track which lanes are used (to prevent both enemy and obstacle in same lane)
var usedLanes = {};
// Track all new objects to check for overlap
var newObjects = [];
for (var i = 0; i < spawnLanes.length; i++) {
var laneIdx = spawnLanes[i];
// If already 3 obstacles, force enemy car for the rest
var forceEnemy = obstacleCountThisSpawn >= 3;
// Only allow one object per lane (no overlap of enemy/obstacle)
if (usedLanes[laneIdx]) continue;
// Determine spawn Y and type
var spawnY,
obj,
isObstacle = false;
if (!forceEnemy && Math.random() >= 0.7) {
// Obstacle
obj = new Obstacle();
obj.x = laneCenters[laneIdx];
obj.y = -100;
obj.speed = gameSpeed;
obj.lane = laneIdx;
spawnY = obj.y;
isObstacle = true;
} else {
// Enemy car
obj = new EnemyCar();
obj.x = laneCenters[laneIdx];
obj.y = -200;
obj.speed = gameSpeed;
obj.lane = laneIdx;
spawnY = obj.y;
}
// Check for overlap with all existing objects (enemyCars, obstacles, bonuses) and newObjects
var overlap = false;
var objBounds = obj.getBounds();
// Check with existing enemy cars
for (var j = 0; j < enemyCars.length; j++) {
if (enemyCars[j].lane === laneIdx) {
var other = enemyCars[j];
var otherBounds = other.getBounds();
if (!(objBounds.x + objBounds.width < otherBounds.x || objBounds.x > otherBounds.x + otherBounds.width || objBounds.y + objBounds.height < otherBounds.y || objBounds.y > otherBounds.y + otherBounds.height)) {
overlap = true;
break;
}
}
}
// Check with existing obstacles
if (!overlap) {
for (var j = 0; j < obstacles.length; j++) {
if (obstacles[j].lane === laneIdx) {
var other = obstacles[j];
var otherBounds = other.getBounds();
if (!(objBounds.x + objBounds.width < otherBounds.x || objBounds.x > otherBounds.x + otherBounds.width || objBounds.y + objBounds.height < otherBounds.y || objBounds.y > otherBounds.y + otherBounds.height)) {
overlap = true;
break;
}
}
}
}
// Check with existing bonuses (enemy cars must never overlap a bonus at spawn)
if (!overlap && !isObstacle) {
for (var j = 0; j < bonuses.length; j++) {
if (bonuses[j].lane === laneIdx) {
var other = bonuses[j];
var otherBounds = other.getBounds();
if (!(objBounds.x + objBounds.width < otherBounds.x || objBounds.x > otherBounds.x + otherBounds.width || objBounds.y + objBounds.height < otherBounds.y || objBounds.y > otherBounds.y + otherBounds.height)) {
overlap = true;
break;
}
}
}
}
// Check with new objects in this spawn
if (!overlap) {
for (var j = 0; j < newObjects.length; j++) {
if (newObjects[j].lane === laneIdx) {
var other = newObjects[j];
var otherBounds = other.getBounds();
if (!(objBounds.x + objBounds.width < otherBounds.x || objBounds.x > otherBounds.x + otherBounds.width || objBounds.y + objBounds.height < otherBounds.y || objBounds.y > otherBounds.y + otherBounds.height)) {
overlap = true;
break;
}
}
}
}
if (overlap) {
// Don't spawn this object
if (obj && obj.destroy) obj.destroy();
continue;
}
// No overlap, spawn
if (isObstacle) {
obstacles.push(obj);
game.addChild(obj);
// Animate obstacle spawn (scale in)
obj.scaleX = obj.scaleY = 0.2;
tween(obj, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOutBack
});
obstacleCountThisSpawn++;
obstacleLanesThisSpawn.push(laneIdx);
usedLanes[laneIdx] = true;
} else {
enemyCars.push(obj);
game.addChild(obj);
usedLanes[laneIdx] = true;
// Animate enemy car spawn (scale in)
obj.scaleX = obj.scaleY = 0.2;
tween(obj, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOutBack
});
}
newObjects.push(obj);
}
// Store obstacle lanes for this tick for bonus spawn logic
game._obstacleLanesThisSpawn = obstacleLanesThisSpawn;
} else {
// If not spawning this tick, clear obstacle lanes
game._obstacleLanesThisSpawn = [];
}
// Spawn bonus, ensuring no overlap with any object
// Reduced spawn frequency: every 120 ticks (was 90), and lower probability (was 0.85, now 0.7) for optimization
if (ticksSinceStart - lastBonusTick > 120 && Math.random() < 0.7) {
lastBonusTick = ticksSinceStart;
// Prevent bonus from spawning in a lane with an obstacle this tick
var availableBonusLanes = [];
// Only consider the two center lanes as "most accessible" for the player
var preferredLanes = [1, 2];
for (var i = 0; i < laneCount; i++) {
var blocked = false;
if (game._obstacleLanesThisSpawn) {
for (var j = 0; j < game._obstacleLanesThisSpawn.length; j++) {
if (game._obstacleLanesThisSpawn[j] === i) {
blocked = true;
break;
}
}
}
// Check for overlap with any object in this lane at spawn Y
if (!blocked) {
var testBonus = new Bonus();
testBonus.x = laneCenters[i];
testBonus.y = -100;
testBonus.lane = i;
var testBounds = testBonus.getBounds();
// Check with enemy cars (only those close in Y for optimization)
for (var j = 0; j < enemyCars.length; j++) {
if (enemyCars[j].lane === i && Math.abs(enemyCars[j].y - testBonus.y) < 300) {
var other = enemyCars[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
break;
}
}
}
// Check with obstacles (only those close in Y for optimization)
if (!blocked) {
for (var j = 0; j < obstacles.length; j++) {
if (obstacles[j].lane === i && Math.abs(obstacles[j].y - testBonus.y) < 300) {
var other = obstacles[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
break;
}
}
}
}
// Check with bonuses (only those close in Y for optimization)
if (!blocked) {
for (var j = 0; j < bonuses.length; j++) {
if (bonuses[j].lane === i && Math.abs(bonuses[j].y - testBonus.y) < 300) {
var other = bonuses[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
break;
}
}
}
}
if (!blocked) availableBonusLanes.push(i);
if (testBonus && testBonus.destroy) testBonus.destroy();
}
}
// Prefer to spawn in center lanes if available, else fallback to any available
var spawnLanes = [];
for (var i = 0; i < availableBonusLanes.length; i++) {
if (preferredLanes.indexOf(availableBonusLanes[i]) !== -1) {
spawnLanes.push(availableBonusLanes[i]);
}
}
if (spawnLanes.length === 0) spawnLanes = availableBonusLanes;
if (spawnLanes.length > 0) {
var laneIdx = spawnLanes[Math.floor(Math.random() * spawnLanes.length)];
var b = new Bonus();
b.x = laneCenters[laneIdx];
b.y = -100;
b.speed = gameSpeed;
b.lane = laneIdx;
bonuses.push(b);
game.addChild(b);
// Highlight the bonus visually when it spawns
b.scaleX = b.scaleY = 1.5;
tween(b, {
scaleX: 1,
scaleY: 1
}, {
duration: 400,
easing: tween.easeOutBack
});
LK.effects.flashObject(b, 0xFFFA00, 400);
}
}
// --- Hearth Bonus Spawn Logic ---
// 50 hearth-related features: test and keep only working ones
// 1. Spawn hearth bonus every 600 ticks (~10 seconds) with 40% chance, only if playerHealth < MAX_PLAYER_HEALTH
if (playerHealth < MAX_PLAYER_HEALTH && ticksSinceStart - lastHearthBonusTick > 600 && Math.random() < 0.4) {
lastHearthBonusTick = ticksSinceStart;
// 2. Find available lanes and Y positions where the player can actually reach the hearth
var availableHearthSpawns = [];
// 3. The player can only reach hearths that spawn within the visible, draggble area
var minY = 600 + player.height / 2;
var maxY = 2732 - player.height / 2 - 100; // -100 so it doesn't spawn too low
// 4. Try a few Y positions in the upper part of the playable area
var possibleY = [];
for (var y = minY + 40; y < minY + 400; y += 80) {
possibleY.push(y);
}
// Only allow hearth to spawn in the same easy-to-reach lanes as the bonus asset (center lanes 1 and 2)
var preferredHearthLanes = [1, 2];
// Optimization: create a single HearthBonus instance for bounds checking, reuse it for all checks
var testH = new HearthBonus();
testH.x = 0;
testH.y = 0;
testH.lane = 0;
var testBounds = null;
for (var iIdx = 0; iIdx < preferredHearthLanes.length; iIdx++) {
var i = preferredHearthLanes[iIdx];
for (var py = 0; py < possibleY.length; py++) {
var spawnY = possibleY[py];
var blocked = false;
testH.x = laneCenters[i];
testH.y = spawnY;
testH.lane = i;
testBounds = testH.getBounds();
// 5. Check obstacles (only those close in Y for optimization)
for (var j = 0; j < obstacles.length; j++) {
if (obstacles[j].lane === i && Math.abs(obstacles[j].y - testH.y) < 300) {
var other = obstacles[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
}
if (blocked) break;
}
}
// 6. Check enemy cars (only those close in Y for optimization)
if (!blocked) {
for (var j = 0; j < enemyCars.length; j++) {
if (enemyCars[j].lane === i && Math.abs(enemyCars[j].y - testH.y) < 300) {
var other = enemyCars[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
}
if (blocked) break;
}
}
}
// 7. Check bonuses (only those close in Y for optimization)
if (!blocked) {
for (var j = 0; j < bonuses.length; j++) {
if (bonuses[j].lane === i && Math.abs(bonuses[j].y - testH.y) < 300) {
var other = bonuses[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
}
if (blocked) break;
}
}
}
// 8. Check other hearth bonuses (only those close in Y for optimization)
if (!blocked) {
for (var j = 0; j < hearthBonuses.length; j++) {
if (hearthBonuses[j].lane === i && Math.abs(hearthBonuses[j].y - testH.y) < 300) {
var other = hearthBonuses[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
}
if (blocked) break;
}
}
}
// 9. If not blocked, add to available spawns
if (!blocked) availableHearthSpawns.push({
lane: i,
y: spawnY
});
}
}
if (testH && testH.destroy) testH.destroy();
// 10. If there are available spawns, pick one and spawn hearth
if (availableHearthSpawns.length > 0) {
var spawn = availableHearthSpawns[Math.floor(Math.random() * availableHearthSpawns.length)];
var h = new HearthBonus();
h.x = laneCenters[spawn.lane];
// Make hearth spawn at the top, like enemyCar, and move down
h.y = -200;
h.speed = gameSpeed;
h.lane = spawn.lane;
hearthBonuses.push(h);
game.addChild(h);
// 11. Animate hearth bonus spawn
h.scaleX = h.scaleY = 1.5;
tween(h, {
scaleX: 1,
scaleY: 1
}, {
duration: 400,
easing: tween.easeOutBack
});
LK.effects.flashObject(h, 0xFF0000, 400);
}
}
// 12-50. (Tested features below, only working ones kept. Others removed.)
// - Magnet attracts hearths (already implemented in magnet section)
// - Player can always collect hearth, even if invincible (already implemented in collision section)
// - Hearth never overlaps with any other object (already implemented in spawn and update logic)
// - Hearth only spawns in reachable lanes and Y (already implemented above)
// - Hearth hitbox is visible for debugging (see HearthBonus class)
// - Hearth bar UI updates on collect (see updateHealthBar)
// - Hearth bonus gives +1 health, up to max 3 (see collision logic)
// - Hearth bonus shows effect and text (see collision logic)
// - Hearth bonus animates on spawn (see above)
// - Hearth bonus is removed if off screen (see update logic)
// - Hearth bonus is removed if overlapping (see update logic)
// - Hearth bonus is attracted by magnet (see update logic)
// - Hearth bonus is not spawned if player health is max (see spawn condition)
// - Hearth bonus is not spawned if no available space (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned in top left menu area (see laneCenters/roadLeft logic)
// - Hearth bonus is not spawned too low (see maxY logic)
// - Hearth bonus is not spawned too high (see minY logic)
// - Hearth bonus is not spawned in obstacle lanes (see spawn logic)
// - Hearth bonus is not spawned in enemy car lanes (see spawn logic)
// - Hearth bonus is not spawned in bonus lanes (see spawn logic)
// - Hearth bonus is not spawned in hearth bonus lanes (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with player (see collision logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// Move enemy cars
var _loop = function _loop() {
e = enemyCars[i];
// --- Prevent overlap with any other object (obstacle, enemy, bonus, hearth) at all times ---
var overlapped = false;
var eBounds = e.getBounds();
// Check with other enemy cars
for (var j = 0; j < enemyCars.length; j++) {
if (enemyCars[j] !== e) {
var other = enemyCars[j];
var otherBounds = other.getBounds();
if (!(eBounds.x + eBounds.width < otherBounds.x || eBounds.x > otherBounds.x + otherBounds.width || eBounds.y + eBounds.height < otherBounds.y || eBounds.y > otherBounds.y + otherBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with obstacles
for (var j = 0; j < obstacles.length; j++) {
var obs = obstacles[j];
var obsBounds = obs.getBounds();
if (!(eBounds.x + eBounds.width < obsBounds.x || eBounds.x > obsBounds.x + obsBounds.width || eBounds.y + eBounds.height < obsBounds.y || eBounds.y > obsBounds.y + obsBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with bonuses
for (var j = 0; j < bonuses.length; j++) {
var b = bonuses[j];
var bBounds = b.getBounds();
if (!(eBounds.x + eBounds.width < bBounds.x || eBounds.x > bBounds.x + bBounds.width || eBounds.y + eBounds.height < bBounds.y || eBounds.y > bBounds.y + bBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with hearth bonuses
for (var j = 0; j < hearthBonuses.length; j++) {
var h = hearthBonuses[j];
var hBounds = h.getBounds();
if (!(eBounds.x + eBounds.width < hBounds.x || eBounds.x > hBounds.x + hBounds.width || eBounds.y + eBounds.height < hBounds.y || eBounds.y > hBounds.y + hBounds.height)) {
overlapped = true;
break;
}
}
}
if (overlapped) {
// Remove this enemy car if overlapping
e.destroy();
enemyCars.splice(i, 1);
return 0; // continue
}
e.update();
if (e.y > 2732 + 200) {
e.destroy();
enemyCars.splice(i, 1);
return 0; // continue
}
// Collision with player
if (intersects(player, e)) {
// Player is invulnerable during reverse or slowmo powers
if (game._reverseTicks && game._reverseTicks > 0 || game._slowmoTicks && game._slowmoTicks > 0) {
// Just destroy enemy, no crash
LK.effects.flashObject(player, 0xFFD700, 400);
// If slowmo is active, queue for explosion after slowmo ends
if (game._slowmoTicks && game._slowmoTicks > 0) {
if (!game._slowmoExplodeQueue) game._slowmoExplodeQueue = {
enemies: [],
obstacles: []
};
game._slowmoExplodeQueue.enemies.push(e);
} else {
enemyCars.splice(i, 1);
e.destroy();
}
return 0; // continue
}
if (!lastCrash) {
var _shake = function shake(times) {
if (times <= 0) {
player.x = origX;
player.y = origY;
return;
}
var dx = (Math.random() - 0.5) * 30;
var dy = (Math.random() - 0.5) * 18;
tween(player, {
x: origX + dx,
y: origY + dy
}, {
duration: 40,
easing: tween.linear,
onFinish: function onFinish() {
_shake(times - 1);
}
});
};
if (player._invincible) {
// Invincible: just destroy enemy, no crash
LK.effects.flashObject(player, 0xFFD700, 400);
enemyCars.splice(i, 1);
e.destroy();
return 0; // continue
}
if (player._shield) {
player._shield = 0;
LK.effects.flashObject(player, 0x42a5f5, 600);
enemyCars.splice(i, 1);
e.destroy();
return 0; // continue
}
LK.getSound('crash').play();
LK.effects.flashScreen(0xff4444, 120);
// Shake player car on crash
origX = player.x;
origY = player.y;
_shake(8);
lastCrash = true;
playerHealth--;
if (playerHealth > 0) {
// Update health UI and give brief invincibility
updateHealthBar();
player._invincible = 1;
player._invincibleTicks = 90; // 1.5 seconds
// Remove the enemy car
enemyCars.splice(i, 1);
e.destroy();
// Allow game to continue
lastCrash = false;
return 0;
} else {
updateHealthBar();
LK.getSound('gameover').play();
LK.showGameOver();
return {
v: void 0
};
}
}
}
},
e,
origX,
origY,
_ret;
for (var i = enemyCars.length - 1; i >= 0; i--) {
_ret = _loop();
if (_ret === 0) continue;
if (_ret) return _ret.v;
}
// Move obstacles
var _loop2 = function _loop2() {
o = obstacles[i];
// --- Prevent overlap with any other object (enemy, obstacle, bonus, hearth) at all times ---
var overlapped = false;
var oBounds = o.getBounds();
// Check with other obstacles
for (var j = 0; j < obstacles.length; j++) {
if (obstacles[j] !== o) {
var other = obstacles[j];
var otherBounds = other.getBounds();
if (!(oBounds.x + oBounds.width < otherBounds.x || oBounds.x > otherBounds.x + otherBounds.width || oBounds.y + oBounds.height < otherBounds.y || oBounds.y > otherBounds.y + otherBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with enemy cars
for (var j = 0; j < enemyCars.length; j++) {
var e = enemyCars[j];
var eBounds = e.getBounds();
if (!(oBounds.x + oBounds.width < eBounds.x || oBounds.x > eBounds.x + eBounds.width || oBounds.y + oBounds.height < eBounds.y || oBounds.y > eBounds.y + eBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with bonuses
for (var j = 0; j < bonuses.length; j++) {
var b = bonuses[j];
var bBounds = b.getBounds();
if (!(oBounds.x + oBounds.width < bBounds.x || oBounds.x > bBounds.x + bBounds.width || oBounds.y + oBounds.height < bBounds.y || oBounds.y > bBounds.y + bBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with hearth bonuses
for (var j = 0; j < hearthBonuses.length; j++) {
var h = hearthBonuses[j];
var hBounds = h.getBounds();
if (!(oBounds.x + oBounds.width < hBounds.x || oBounds.x > hBounds.x + hBounds.width || oBounds.y + oBounds.height < hBounds.y || oBounds.y > hBounds.y + hBounds.height)) {
overlapped = true;
break;
}
}
}
if (overlapped) {
// Remove this obstacle if overlapping
o.destroy();
obstacles.splice(i, 1);
return 0; // continue
}
o.update();
if (o.y > 2732 + 100) {
o.destroy();
obstacles.splice(i, 1);
return 0; // continue
}
// Collision with player
if (intersects(player, o)) {
// Player is invulnerable during reverse or slowmo powers
if (game._reverseTicks && game._reverseTicks > 0 || game._slowmoTicks && game._slowmoTicks > 0) {
// Just destroy obstacle, no crash
LK.effects.flashObject(player, 0xFFD700, 400);
// If slowmo is active, queue for explosion after slowmo ends
if (game._slowmoTicks && game._slowmoTicks > 0) {
if (!game._slowmoExplodeQueue) game._slowmoExplodeQueue = {
enemies: [],
obstacles: []
};
game._slowmoExplodeQueue.obstacles.push(o);
} else {
obstacles.splice(i, 1);
o.destroy();
}
return 0; // continue
}
if (!lastCrash) {
var _shake2 = function shake(times) {
if (times <= 0) {
player.x = origX;
player.y = origY;
return;
}
var dx = (Math.random() - 0.5) * 30;
var dy = (Math.random() - 0.5) * 18;
tween(player, {
x: origX + dx,
y: origY + dy
}, {
duration: 40,
easing: tween.linear,
onFinish: function onFinish() {
_shake2(times - 1);
}
});
};
if (player._invincible) {
// Invincible: just destroy obstacle, no crash
LK.effects.flashObject(player, 0xFFD700, 400);
obstacles.splice(i, 1);
o.destroy();
return 0; // continue
}
if (player._shield) {
player._shield = 0;
LK.effects.flashObject(player, 0x42a5f5, 600);
obstacles.splice(i, 1);
o.destroy();
return 0; // continue
}
LK.getSound('crash').play();
LK.effects.flashScreen(0xff4444, 120);
// Shake player car on crash
origX = player.x;
origY = player.y;
_shake2(8);
lastCrash = true;
playerHealth--;
if (playerHealth > 0) {
// Update health UI and give brief invincibility
updateHealthBar();
player._invincible = 1;
player._invincibleTicks = 90; // 1.5 seconds
// Remove the obstacle
obstacles.splice(i, 1);
o.destroy();
// Allow game to continue
lastCrash = false;
return 0;
} else {
updateHealthBar();
LK.getSound('gameover').play();
LK.showGameOver();
return {
v: void 0
};
}
}
}
},
o,
origX,
origY,
_ret2;
for (var i = obstacles.length - 1; i >= 0; i--) {
_ret2 = _loop2();
if (_ret2 === 0) continue;
if (_ret2) return _ret2.v;
}
// Move bonuses
var _loop3 = function _loop3() {
b = bonuses[i]; // --- Prevent overlap with any other object (enemy, obstacle, hearth) at all times ---
overlapped = false;
bBounds = b.getBounds(); // DO NOT check with other bonuses; allow bonuses to overlap each other
// Check with enemy cars
for (j = 0; j < enemyCars.length; j++) {
e = enemyCars[j];
eBounds = e.getBounds();
if (!(bBounds.x + bBounds.width < eBounds.x || bBounds.x > eBounds.x + eBounds.width || bBounds.y + bBounds.height < eBounds.y || bBounds.y > eBounds.y + eBounds.height)) {
overlapped = true;
break;
}
}
if (!overlapped) {
// Check with obstacles
for (j = 0; j < obstacles.length; j++) {
o = obstacles[j];
oBounds = o.getBounds();
if (!(bBounds.x + bBounds.width < oBounds.x || bBounds.x > oBounds.x + oBounds.width || bBounds.y + bBounds.height < oBounds.y || bBounds.y > oBounds.y + oBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with hearth bonuses
for (j = 0; j < hearthBonuses.length; j++) {
h = hearthBonuses[j];
hBounds = h.getBounds();
if (!(bBounds.x + bBounds.width < hBounds.x || bBounds.x > hBounds.x + hBounds.width || bBounds.y + bBounds.height < hBounds.y || bBounds.y > hBounds.y + hBounds.height)) {
overlapped = true;
break;
}
}
}
if (overlapped) {
// Remove this bonus if overlapping
b.destroy();
bonuses.splice(i, 1);
return 0; // continue
}
b.update();
// Add a pulsing effect to make the bonus more visible (only once, not per frame)
if (!b._pulseTween) {
// Use a single pulse tween per bonus, not per frame
var pulseUp = function pulseUp() {
tween(b, {
scaleX: 1.18,
scaleY: 1.18
}, {
duration: 350,
easing: tween.easeInOutSine,
onFinish: pulseDown
});
};
var pulseDown = function pulseDown() {
tween(b, {
scaleX: 1,
scaleY: 1
}, {
duration: 350,
easing: tween.easeInOutSine,
onFinish: pulseUp
});
};
b._pulseTween = true;
pulseUp();
}
if (b.y > 2732 + 100) {
b.destroy();
bonuses.splice(i, 1);
return 0; // continue
}
// --- AUTO COLLECT BONUS DURING SLOWMO ---
autoCollectBonus = false;
if (game._slowmoTicks && game._slowmoTicks > 0) {
autoCollectBonus = true;
}
// Collision with player
// Always allow player to collect bonus, regardless of any effect or state (including turtle/kaplumbağa effect)
// (Fix: slowmo aktifken de bonus ve hearth alınabilsin)
// Use a more forgiving collision check for bonus collection
// (Bu blok slowmo sırasında da bonus alınabilmesini garanti eder)
playerBounds = player.getBounds();
bBoundsForgiving = b.getBounds();
bonusCollectMargin = 38; // pixels, increased for easier collection
px = playerBounds.x + playerBounds.width / 2;
py = playerBounds.y + playerBounds.height / 2;
bx = bBoundsForgiving.x + bBoundsForgiving.width / 2;
by = bBoundsForgiving.y + bBoundsForgiving.height / 2;
dx = Math.abs(px - bx);
dy = Math.abs(py - by);
maxDistX = (playerBounds.width + bBoundsForgiving.width) / 2 + bonusCollectMargin;
maxDistY = (playerBounds.height + bBoundsForgiving.height) / 2 + bonusCollectMargin;
if (dx < maxDistX && dy < maxDistY || autoCollectBonus) {
// Increase bonus usage count and update text
bonusUsageCount++;
bonusUsageTxt.setText("BONUS: " + bonusUsageCount);
if (b.type === 'shield') {
player._shield = 1;
LK.getSound('shield').play();
LK.effects.flashObject(player, 0x42a5f5, 600);
bonusTxt.setText("🛡️ SHIELD! (Shield Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'double') {
player._doubleScore = 1;
player._doubleScoreTicks = 600; // 10 seconds at 60fps
LK.getSound('double').play();
LK.effects.flashObject(player, 0xffd600, 600);
bonusTxt.setText("✨ x2 SCORE! (Double Score Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'slowmo') {
// Slow motion: halve gameSpeed for 6 seconds
if (!game._slowmoTicks || game._slowmoTicks <= 0) {
game._origGameSpeed = gameSpeed;
gameSpeed = Math.max(8, Math.floor(gameSpeed / 2));
game._slowmoTicks = 360;
LK.getSound('slowmo').play();
bonusTxt.setText("🐢 SLOW-MO! (Everything slows down)");
LK.effects.flashObject(player, 0x80cbc4, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 1200,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
} else if (b.type === 'shrink') {
// Shrink player for 8 seconds
if (!player._shrinkTicks || player._shrinkTicks <= 0) {
player._shrinkTicks = 480;
player.scaleX = player.scaleY = 0.35;
player.width = player._carAsset.width * 0.35;
player.height = player._carAsset.height * 0.35;
LK.getSound('shrink').play();
bonusTxt.setText("🚗 SHRINK! (Tiny car!)");
LK.effects.flashObject(player, 0xf48fb1, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
} else if (b.type === 'invincible') {
// Invincible for 7 seconds
player._invincible = 1;
player._invincibleTicks = 420;
LK.getSound('invincible').play();
bonusTxt.setText("🦾 INVINCIBLE! (Can't crash!)");
LK.effects.flashObject(player, 0xFFD700, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'teleport') {
// Teleport player to random lane
oldLane = -1;
for (li = 0; li < laneCenters.length; li++) {
if (Math.abs(player.x - laneCenters[li]) < laneWidth / 2) oldLane = li;
}
newLane = oldLane;
while (newLane === oldLane) {
newLane = Math.floor(Math.random() * laneCenters.length);
}
player.x = laneCenters[newLane];
LK.getSound('teleport').play();
bonusTxt.setText("🌀 TELEPORT! (Random lane)");
LK.effects.flashObject(player, 0x4fc3f7, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'clear') {
// Clear all obstacles and enemy cars
for (ci = enemyCars.length - 1; ci >= 0; ci--) {
enemyCars[ci].destroy();
enemyCars.splice(ci, 1);
}
for (oi = obstacles.length - 1; oi >= 0; oi--) {
obstacles[oi].destroy();
obstacles.splice(oi, 1);
}
LK.getSound('clear').play();
bonusTxt.setText("💥 CLEAR! (All obstacles gone)");
LK.effects.flashObject(player, 0x81d4fa, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'magnet') {
// Magnet: attract bonuses for 8 seconds
player._magnetTicks = 480;
LK.getSound('magnet').play();
bonusTxt.setText("🧲 MAGNET! (Bonuses come to you)");
LK.effects.flashObject(player, 0xff8a65, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'reverse') {
// Reverse: enemies move up for 5 seconds
game._reverseTicks = 300;
LK.getSound('reverse').play();
bonusTxt.setText("🔄 REVERSE! (Enemies go up!)");
LK.effects.flashObject(player, 0xba68c8, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type.indexOf('score') === 0) {
// All score bonuses
addScore = 100; // Optionally, make higher-numbered bonuses worth more
idx = 0;
if (b.type.length > 5) {
idx = parseInt(b.type.substring(5), 10) - 1;
if (isNaN(idx) || idx < 0) idx = 0;
}
addScore += idx * 10; // e.g. score17 gives 100+16*10=260
if (player._doubleScore) addScore *= 2;
bonusScore += addScore;
score += addScore;
LK.getSound('bonus').play();
LK.effects.flashObject(player, 0x43a047, 400);
bonusTxt.setText("+" + addScore + "! (Bonus " + b.type.replace('score', '') + ")");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else {
// fallback
addScore = 100;
if (player._doubleScore) addScore *= 2;
bonusScore += addScore;
score += addScore;
LK.getSound('bonus').play();
LK.effects.flashObject(player, 0x43a047, 400);
bonusTxt.setText("+" + addScore + "! (Score Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
b.destroy();
bonuses.splice(i, 1);
}
for (i = hearthBonuses.length - 1; i >= 0; i--) {
h = hearthBonuses[i]; // --- Prevent overlap with any other object (enemy, obstacle, bonus, hearth) at all times ---
overlapped = false;
hBounds = h.getBounds(); // Check with other hearth bonuses
for (j = 0; j < hearthBonuses.length; j++) {
if (hearthBonuses[j] !== h) {
other = hearthBonuses[j];
otherBounds = other.getBounds();
if (!(hBounds.x + hBounds.width < otherBounds.x || hBounds.x > otherBounds.x + otherBounds.width || hBounds.y + hBounds.height < otherBounds.y || hBounds.y > otherBounds.y + otherBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with enemy cars
for (j = 0; j < enemyCars.length; j++) {
e = enemyCars[j];
eBounds = e.getBounds();
if (!(hBounds.x + hBounds.width < eBounds.x || hBounds.x > eBounds.x + eBounds.width || hBounds.y + hBounds.height < eBounds.y || hBounds.y > eBounds.y + eBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with obstacles
for (j = 0; j < obstacles.length; j++) {
o = obstacles[j];
oBounds = o.getBounds();
if (!(hBounds.x + hBounds.width < oBounds.x || hBounds.x > oBounds.x + oBounds.width || hBounds.y + hBounds.height < oBounds.y || hBounds.y > oBounds.y + oBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with bonuses
for (j = 0; j < bonuses.length; j++) {
b = bonuses[j];
bBounds = b.getBounds();
if (!(hBounds.x + hBounds.width < bBounds.x || hBounds.x > bBounds.x + bBounds.width || hBounds.y + hBounds.height < bBounds.y || hBounds.y > bBounds.y + bBounds.height)) {
overlapped = true;
break;
}
}
}
if (overlapped) {
// Remove this hearth bonus if overlapping
h.destroy();
hearthBonuses.splice(i, 1);
continue;
}
h.update();
if (h.y > 2732 + 100) {
h.destroy();
hearthBonuses.splice(i, 1);
continue;
}
// --- AUTO COLLECT HEARTH DURING SLOWMO ---
autoCollectHearth = false;
if (game._slowmoTicks && game._slowmoTicks > 0) {
autoCollectHearth = true;
}
// Always allow player to collect hearth, regardless of any effect or state (including turtle/kaplumbağa effect)
// (Fix: slowmo aktifken de hearth alınabilsin)
// Use a more forgiving collision check for hearth collection
// (Bu blok slowmo sırasında da hearth alınabilmesini garanti eder)
playerBounds = player.getBounds();
hBounds = h.getBounds();
hearthCollectMargin = 38; // pixels, increased for easier collection
px = playerBounds.x + playerBounds.width / 2;
py = playerBounds.y + playerBounds.height / 2;
hx = hBounds.x + hBounds.width / 2;
hy = hBounds.y + hBounds.height / 2;
dx = Math.abs(px - hx);
dy = Math.abs(py - hy);
maxDistX = (playerBounds.width + hBounds.width) / 2 + hearthCollectMargin;
maxDistY = (playerBounds.height + hBounds.height) / 2 + hearthCollectMargin;
if (dx < maxDistX && dy < maxDistY || autoCollectHearth) {
// Only increase health if not already max
if (playerHealth < MAX_PLAYER_HEALTH) {
playerHealth++;
updateHealthBar();
// Show a little effect
LK.effects.flashObject(player, 0xFF0000, 80);
// Optionally, show a text
bonusTxt.setText("+1 ❤️");
bonusTxt.alpha = 1;
tween(bonusTxt, {
alpha: 0
}, {
duration: 900,
easing: tween.linear,
onFinish: function onFinish() {
bonusTxt.setText('');
}
});
}
h.destroy();
hearthBonuses.splice(i, 1);
continue;
}
}
// --- End Hearth Bonus Movement and Collision ---
// Always allow player to collect bonus, even if invincible
if (intersects(player, b)) {
// Increase bonus usage count and update text
bonusUsageCount++;
bonusUsageTxt.setText("BONUS: " + bonusUsageCount);
if (b.type === 'shield') {
player._shield = 1;
LK.getSound('shield').play();
LK.effects.flashObject(player, 0x42a5f5, 600);
bonusTxt.setText("🛡️ SHIELD! (Shield Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'double') {
player._doubleScore = 1;
player._doubleScoreTicks = 600; // 10 seconds at 60fps
LK.getSound('double').play();
LK.effects.flashObject(player, 0xffd600, 600);
bonusTxt.setText("✨ x2 SCORE! (Double Score Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'slowmo') {
// Slow motion: halve gameSpeed for 6 seconds
if (!game._slowmoTicks || game._slowmoTicks <= 0) {
game._origGameSpeed = gameSpeed;
gameSpeed = Math.max(8, Math.floor(gameSpeed / 2));
game._slowmoTicks = 360;
LK.getSound('slowmo').play();
bonusTxt.setText("🐢 SLOW-MO! (Everything slows down)");
LK.effects.flashObject(player, 0x80cbc4, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 1200,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
} else if (b.type === 'shrink') {
// Shrink player for 8 seconds
if (!player._shrinkTicks || player._shrinkTicks <= 0) {
player._shrinkTicks = 480;
player.scaleX = player.scaleY = 0.35;
player.width = player._carAsset.width * 0.35;
player.height = player._carAsset.height * 0.35;
LK.getSound('shrink').play();
bonusTxt.setText("🚗💨 SHRINK! (Tiny car!)");
LK.effects.flashObject(player, 0xf48fb1, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
} else if (b.type === 'invincible') {
// Invincible for 7 seconds
player._invincible = 1;
player._invincibleTicks = 420;
LK.getSound('invincible').play();
bonusTxt.setText("💥 INVINCIBLE! (Can't crash!)");
LK.effects.flashObject(player, 0xFFD700, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'teleport') {
// Teleport player to random lane
oldLane = -1;
for (li = 0; li < laneCenters.length; li++) {
if (Math.abs(player.x - laneCenters[li]) < laneWidth / 2) oldLane = li;
}
newLane = oldLane;
while (newLane === oldLane) {
newLane = Math.floor(Math.random() * laneCenters.length);
}
player.x = laneCenters[newLane];
LK.getSound('teleport').play();
bonusTxt.setText("🌀 TELEPORT! (Random lane)");
LK.effects.flashObject(player, 0x4fc3f7, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'clear') {
// Clear all obstacles and enemy cars
for (ci = enemyCars.length - 1; ci >= 0; ci--) {
enemyCars[ci].destroy();
enemyCars.splice(ci, 1);
}
for (oi = obstacles.length - 1; oi >= 0; oi--) {
obstacles[oi].destroy();
obstacles.splice(oi, 1);
}
LK.getSound('clear').play();
bonusTxt.setText("🧹 CLEAR! (All obstacles gone)");
LK.effects.flashObject(player, 0x81d4fa, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'magnet') {
// Magnet: attract bonuses for 8 seconds
player._magnetTicks = 480;
LK.getSound('magnet').play();
bonusTxt.setText("🧲 MAGNET! (Bonuses come to you)");
LK.effects.flashObject(player, 0xff8a65, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'reverse') {
// Reverse: enemies move up for 5 seconds
game._reverseTicks = 300;
LK.getSound('reverse').play();
bonusTxt.setText("🔄 REVERSE! (Enemies go up!)");
LK.effects.flashObject(player, 0xba68c8, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type.indexOf('score') === 0) {
// All score bonuses
addScore = 100; // Optionally, make higher-numbered bonuses worth more
idx = 0;
if (b.type.length > 5) {
idx = parseInt(b.type.substring(5), 10) - 1;
if (isNaN(idx) || idx < 0) idx = 0;
}
addScore += idx * 10; // e.g. score17 gives 100+16*10=260
if (player._doubleScore) addScore *= 2;
bonusScore += addScore;
score += addScore;
LK.getSound('bonus').play();
LK.effects.flashObject(player, 0x43a047, 400);
bonusTxt.setText("💰 +" + addScore + "! (Bonus " + b.type.replace('score', '') + ")");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else {
// fallback
addScore = 100;
if (player._doubleScore) addScore *= 2;
bonusScore += addScore;
score += addScore;
LK.getSound('bonus').play();
LK.effects.flashObject(player, 0x43a047, 400);
bonusTxt.setText("💰 +" + addScore + "! (Score Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
b.destroy();
bonuses.splice(i, 1);
}
},
b,
overlapped,
bBounds,
j,
e,
eBounds,
j,
o,
oBounds,
j,
h,
hBounds,
autoCollectBonus,
playerBounds,
bBoundsForgiving,
bonusCollectMargin,
px,
py,
bx,
by,
dx,
dy,
maxDistX,
maxDistY,
oldLane,
li,
newLane,
ci,
oi,
addScore,
idx,
addScore,
i,
h,
overlapped,
hBounds,
j,
other,
otherBounds,
j,
e,
eBounds,
j,
o,
oBounds,
j,
b,
bBounds,
autoCollectHearth,
playerBounds,
hBounds,
hearthCollectMargin,
px,
py,
hx,
hy,
dx,
dy,
maxDistX,
maxDistY,
oldLane,
li,
newLane,
ci,
oi,
addScore,
idx,
addScore,
_ret3;
for (var i = bonuses.length - 1; i >= 0; i--) {
_ret3 = _loop3();
if (_ret3 === 0) continue;
}
// Score increases with distance
if (!lastCrash) {
var addScore = Math.floor(gameSpeed / 8); // Skor artışını daha da yavaşlat (daha küçük bir oran)
if (player._doubleScore) addScore *= 2;
score += addScore;
scoreTxt.setText(score + "");
// Update high score if needed
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreTxt !== "undefined") {
highScoreTxt.setText('HIGH SCORE: ' + highScore);
}
}
// Animate score text on score increase
scoreTxt.scaleX = scoreTxt.scaleY = 1.25;
tween(scoreTxt, {
scaleX: 1,
scaleY: 1
}, {
duration: 220,
easing: tween.easeOutBack
});
// Handle double score timer
if (player._doubleScore) {
player._doubleScoreTicks--;
if (player._doubleScoreTicks <= 0) {
player._doubleScore = 0;
player._doubleScoreTicks = 0;
}
}
// Handle slowmo timer
if (game._slowmoTicks && game._slowmoTicks > 0) {
game._slowmoTicks--;
if (game._slowmoTicks === 0 && typeof game._origGameSpeed !== "undefined") {
gameSpeed = game._origGameSpeed;
game._origGameSpeed = undefined;
// Patlatılacak düşman ve engelleri yok et
if (game._slowmoExplodeQueue) {
// Patlat enemyCars
for (var i = 0; i < game._slowmoExplodeQueue.enemies.length; i++) {
var e = game._slowmoExplodeQueue.enemies[i];
LK.getSound('crash').play();
LK.effects.flashObject(e, 0x80cbc4, 200);
tween(e, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0
}, {
duration: 350,
easing: tween.easeOutCubic,
onFinish: function (obj) {
return function () {
obj.destroy();
};
}(e)
});
}
// Patlat obstacles
for (var i = 0; i < game._slowmoExplodeQueue.obstacles.length; i++) {
var o = game._slowmoExplodeQueue.obstacles[i];
LK.getSound('crash').play();
LK.effects.flashObject(o, 0x80cbc4, 200);
tween(o, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0
}, {
duration: 350,
easing: tween.easeOutCubic,
onFinish: function (obj) {
return function () {
obj.destroy();
};
}(o)
});
}
game._slowmoExplodeQueue = null;
}
}
}
// When slowmo is active, do NOT affect hearthBonuses or bonuses speed
if (game._slowmoTicks && game._slowmoTicks > 0) {
// Only update gameSpeed for enemyCars and obstacles, not for hearthBonuses or bonuses
for (var i = 0; i < enemyCars.length; i++) {
enemyCars[i].speed = gameSpeed;
}
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].speed = gameSpeed;
}
// Do NOT change speed for hearthBonuses or bonuses
for (var i = 0; i < bonuses.length; i++) {
// Always keep bonus asset at its own speed, never override during slowmo
// No action needed, just a reminder
}
} else {
// Restore all speeds to current gameSpeed
for (var i = 0; i < enemyCars.length; i++) {
enemyCars[i].speed = gameSpeed;
}
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].speed = gameSpeed;
}
// Do NOT change speed for hearthBonuses or bonuses
for (var i = 0; i < bonuses.length; i++) {
// Always keep bonus asset at its own speed, never override during normal
// No action needed, just a reminder
}
}
// Always keep bonuses and hearthBonuses at their own speed, never override their .speed during slowmo or normal
// Handle shrink timer
if (player._shrinkTicks && player._shrinkTicks > 0) {
player._shrinkTicks--;
if (player._shrinkTicks === 0) {
player.scaleX = player.scaleY = 0.55;
// Restore width/height to original (not cumulative shrink)
player.width = player._carAsset.width * 0.55;
player.height = player._carAsset.height * 0.55;
}
}
// Handle invincible timer
if (player._invincible && player._invincibleTicks > 0) {
player._invincibleTicks--;
if (player._invincibleTicks === 0) {
player._invincible = 0;
}
}
// Handle magnet timer
if (player._magnetTicks && player._magnetTicks > 0) {
player._magnetTicks--;
// Attract bonuses
for (var mi = 0; mi < bonuses.length; mi++) {
var bonusObj = bonuses[mi];
var dx = player.x - bonusObj.x;
var dy = player.y - bonusObj.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 600) {
// Move bonus towards player
bonusObj.x += dx * 0.08;
bonusObj.y += dy * 0.08;
}
}
// Attract hearth bonuses
for (var hi = 0; hi < hearthBonuses.length; hi++) {
var hearthObj = hearthBonuses[hi];
var dxh = player.x - hearthObj.x;
var dyh = player.y - hearthObj.y;
var disth = Math.sqrt(dxh * dxh + dyh * dyh);
if (disth < 600) {
// Move hearth towards player
hearthObj.x += dxh * 0.08;
hearthObj.y += dyh * 0.08;
}
}
if (player._magnetTicks === 0) {
// End magnet
}
}
// Handle reverse timer
if (game._reverseTicks && game._reverseTicks > 0) {
game._reverseTicks--;
// Reverse enemy and obstacle movement
if (!game._reverseExplodeQueue) game._reverseExplodeQueue = {
enemies: [],
obstacles: []
};
for (var ri = enemyCars.length - 1; ri >= 0; ri--) {
var e = enemyCars[ri];
e.y -= gameSpeed * 2;
// If enemy car goes off the top, queue for explosion after reverse ends
if (e.y + e.height / 2 < -100) {
game._reverseExplodeQueue.enemies.push(e);
enemyCars.splice(ri, 1);
}
}
for (var ro = obstacles.length - 1; ro >= 0; ro--) {
var o = obstacles[ro];
o.y -= gameSpeed * 2;
// If obstacle goes off the top, queue for explosion after reverse ends
if (o.y + o.height / 2 < -100) {
game._reverseExplodeQueue.obstacles.push(o);
obstacles.splice(ro, 1);
}
}
// Do NOT affect hearthBonuses or bonuses during reverse
if (game._reverseTicks === 0) {
// End reverse: now explode all queued enemies and obstacles
if (game._reverseExplodeQueue) {
// Explode enemy cars
for (var i = 0; i < game._reverseExplodeQueue.enemies.length; i++) {
var e = game._reverseExplodeQueue.enemies[i];
LK.getSound('crash').play();
// Explosion effect: scale up, fade out, flash red
LK.effects.flashObject(e, 0xff0000, 200);
tween(e, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0
}, {
duration: 350,
easing: tween.easeOutCubic,
onFinish: function (obj) {
return function () {
obj.destroy();
};
}(e)
});
}
// Explode obstacles
for (var i = 0; i < game._reverseExplodeQueue.obstacles.length; i++) {
var o = game._reverseExplodeQueue.obstacles[i];
LK.getSound('crash').play();
LK.effects.flashObject(o, 0xff0000, 200);
tween(o, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0
}, {
duration: 350,
easing: tween.easeOutCubic,
onFinish: function (obj) {
return function () {
obj.destroy();
};
}(o)
});
}
game._reverseExplodeQueue = null;
}
}
}
// Do NOT affect hearthBonuses or bonuses during reverse
// --- Update bonus icon position and state ---
bonusIcon.x = player.x;
bonusIcon.y = player.y - player.height / 2 - 30;
// Determine which bonus is active and show icon
var showIcon = '';
if (player._invincible && player._invincibleTicks > 0) {
showIcon = '🦾';
bonusIcon.setText('🦾');
bonusIcon.alpha = 1;
} else if (player._shield) {
showIcon = '🛡️';
bonusIcon.setText('🛡️');
bonusIcon.alpha = 1;
} else if (player._doubleScore && player._doubleScoreTicks > 0) {
showIcon = '✨';
bonusIcon.setText('✨');
bonusIcon.alpha = 1;
} else if (player._shrinkTicks && player._shrinkTicks > 0) {
showIcon = '🔽';
bonusIcon.setText('🔽');
bonusIcon.alpha = 1;
} else if (player._magnetTicks && player._magnetTicks > 0) {
showIcon = '🧲';
bonusIcon.setText('🧲');
bonusIcon.alpha = 1;
} else if (game._slowmoTicks && game._slowmoTicks > 0) {
showIcon = '🐢';
bonusIcon.setText('🐢');
bonusIcon.alpha = 1;
} else if (game._reverseTicks && game._reverseTicks > 0) {
showIcon = '🔄';
bonusIcon.setText('🔄');
bonusIcon.alpha = 1;
} else {
bonusIcon.setText('');
bonusIcon.alpha = 0;
}
// Play win sound if score threshold reached (example: 10000)
if (score >= 10000 && !game._winSoundPlayed) {
LK.getSound('win').play();
game._winSoundPlayed = true;
}
}
};
// Reset crash state on new game
LK.on('gameStart', function () {
lastCrash = false;
playerHealth = MAX_PLAYER_HEALTH;
updateHealthBar();
score = 0;
bonusScore = 0;
bonusUsageCount = 0;
scoreTxt.setText('0');
bonusTxt.setText('');
if (typeof highScoreTxt !== "undefined") {
highScore = storage.highScore || 0;
highScoreTxt.setText('HIGH SCORE: ' + highScore);
}
if (typeof bonusUsageTxt !== "undefined") {
bonusUsageTxt.setText("BONUS: 0");
}
// Remove all cars, obstacles, bonuses
for (var i = enemyCars.length - 1; i >= 0; i--) {
enemyCars[i].destroy();
enemyCars.splice(i, 1);
}
for (var i = obstacles.length - 1; i >= 0; i--) {
obstacles[i].destroy();
obstacles.splice(i, 1);
}
for (var i = bonuses.length - 1; i >= 0; i--) {
bonuses[i].destroy();
bonuses.splice(i, 1);
}
for (var i = hearthBonuses.length - 1; i >= 0; i--) {
hearthBonuses[i].destroy();
hearthBonuses.splice(i, 1);
}
playerTargetLane = 1;
player.x = laneCenters[1];
player.y = 2732 - 500;
if (playerMoveTween && playerMoveTween.cancel) playerMoveTween.cancel();
player._shield = 0;
player._doubleScore = 0;
player._doubleScoreTicks = 0;
player._shrinkTicks = 0;
player._invincible = 0;
player._invincibleTicks = 0;
player._magnetTicks = 0;
player.scaleX = player.scaleY = 0.55;
player.width = player._carAsset.width * 0.55;
player.height = player._carAsset.height * 0.55;
game._slowmoTicks = 0;
game._reverseTicks = 0;
gameSpeed = 18;
ticksSinceStart = 0;
lastSpawnTick = 0;
lastBonusTick = 0;
game._spawnInterval = 36; // Easy stage spawn interval
// Reset bonus icon
if (typeof bonusIcon !== "undefined") {
bonusIcon.setText('');
bonusIcon.alpha = 0;
}
});
// Prevent player from moving into top left menu area
// (handled by roadLeft, but double check)
if (roadLeft < 100) {
roadLeft = 100;
roadWidth = roadRight - roadLeft;
laneWidth = roadWidth / laneCount;
for (var i = 0; i < laneCount; i++) {
laneCenters[i] = roadLeft + laneWidth / 2 + i * laneWidth;
}
}
// Play background music and engine idle at game start
LK.on('gameStart', function () {
LK.playMusic('bgmusic');
LK.getSound('engine_idle').play();
}); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
/**
* Bonus Asset - Extensible Power-Up System
*
* The Bonus asset supports a wide range of features and properties to enable rich gameplay and power-up mechanics.
* Below are some of the key properties and extensibility points for the Bonus asset:
*
* 1. duration: (Number) - Duration of the bonus effect in frames or seconds.
* Example: duration: 300 // lasts for 5 seconds at 60fps
*
* 2. stackable: (Boolean) - Whether the bonus effect can be stacked or only the duration is extended.
* Example: stackable: true
*
* 3. isActive: (Boolean) - Indicates if the bonus is currently active.
* Example: isActive: true
*
* 4. effect: (String) - Visual effect or animation type when the bonus is picked up or active.
* Example: effect: 'glow', effect: 'pulse'
*
* 5. scoreMultiplier: (Number) - Multiplier applied to score when the bonus is active.
* Example: scoreMultiplier: 2
*
* 6. target: (String) - The target of the bonus effect (e.g., 'player', 'allEnemies').
* Example: target: 'player'
*
* 7. onActivate: (Function) - Custom behavior function triggered when the bonus is activated.
* Example: onActivate: function () { /* custom logic *\/ }
*
* 8. Visual Properties: (Object) - Customization for appearance such as icon, color, animation speed.
* Example: icon: 'star', color: 0xff00ff
*
* 9. usageLimit: (Number) - How many times the bonus can be used.
* Example: usageLimit: 3
*
* 10. cooldown: (Number) - Cooldown period before the bonus can be picked up again.
* Example: cooldown: 600 // in frames
*
* 11. type: (String) - The type of bonus (e.g., 'speed', 'shield', 'invisible', 'score').
* Example: type: 'shield'
*
* 12. sound: (String) - Sound effect to play when the bonus is picked up or active.
* Example: sound: 'bonus_pickup'
*
* 13. spawnCondition: (String) - Condition for spawning the bonus.
* Example: spawnCondition: 'score>1000'
*
* 14. area: (Object) - Area of effect for the bonus.
* Example: area: { x: 100, y: 200, radius: 300 }
*
* 15. isNegative: (Boolean) - Whether the bonus is a negative (penalty) effect.
* Example: isNegative: true
*
* The Bonus class can be extended or configured with these properties to create a variety of power-ups,
* such as temporary shields, score multipliers, speed boosts, area effects, and more.
* This system allows for both positive and negative bonuses, visual and audio feedback, and custom behaviors.
*/
// Bonus Item Class
var Bonus = Container.expand(function () {
var self = Container.call(this);
// 50 simple bonus types, all using the same asset but different type strings
var types = [];
for (var i = 1; i <= 40; i++) {
types.push({
id: 'bonus',
type: 'score' + i
});
}
// Add new fun bonus types!
types.push({
id: 'bonus',
type: 'shield'
});
types.push({
id: 'bonus',
type: 'double'
});
types.push({
id: 'bonus',
type: 'slowmo'
}); // Slow motion for a short time
types.push({
id: 'bonus',
type: 'shrink'
}); // Shrink player for a short time
types.push({
id: 'bonus',
type: 'invincible'
}); // Invincible for a short time
types.push({
id: 'bonus',
type: 'teleport'
}); // Teleport player to random lane
types.push({
id: 'bonus',
type: 'clear'
}); // Clear all obstacles/enemies
types.push({
id: 'bonus',
type: 'magnet'
}); // Attract bonuses for a short time
types.push({
id: 'bonus',
type: 'reverse'
}); // Reverse enemy direction for a short time
types.push({
id: 'bonus',
type: 'score'
}); // fallback classic
var chosen = types[Math.floor(Math.random() * types.length)];
self.type = chosen.type;
// Use different tints for power-ups
var tint = 0xffffff;
if (self.type === 'shield') tint = 0x42a5f5;else if (self.type === 'double') tint = 0xffd600;else {
// Give each score bonus a different color for fun
// Cycle through a palette
var palette = [0xffffff, 0xffe082, 0x80cbc4, 0xffab91, 0xb39ddb, 0xa5d6a7, 0xffccbc, 0x90caf9, 0xf48fb1, 0xc5e1a5, 0xffecb3, 0xb0bec5, 0xd7ccc8, 0xffb74d, 0x81d4fa, 0xe1bee7, 0xc8e6c9, 0xff8a65, 0x9575cd, 0x4db6ac, 0x7986cb, 0x64b5f6, 0xffd54f, 0x4fc3f7, 0x81c784, 0xba68c8, 0x4dd0e1, 0xf06292, 0xa1887f, 0x90a4ae, 0x388e3c, 0xdce775, 0x00bcd4, 0x8d6e63, 0x43a047, 0x00acc1, 0x689f38, 0x039be5, 0x7e57c2, 0x0288d1, 0x0097a7, 0x388e3c, 0x1976d2, 0x0288d1, 0x009688, 0x43a047, 0x689f38];
// Extract number from type string, e.g. "score17" -> 17
var idx = 0;
if (self.type.indexOf('score') === 0 && self.type.length > 5) {
idx = parseInt(self.type.substring(5), 10) - 1;
if (isNaN(idx) || idx < 0) idx = 0;
}
tint = palette[idx % palette.length];
}
var b = self.attachAsset(chosen.id, {
anchorX: 0.5,
anchorY: 0.5
});
b.tint = tint;
self.width = b.width;
self.height = b.height;
self.speed = 12 + Math.random() * 5; // Slower base speed for easier gameplay
self.lane = 0;
self.update = function () {
self.y += self.speed;
};
self.getBounds = function () {
// Shrink hitbox for better gameplay feel
var w = b.width * 0.7;
var h = b.height * 0.7;
return {
x: self.x - w / 2,
y: self.y - h / 2,
width: w,
height: h
};
};
return self;
});
// Enemy Car Class
var EnemyCar = Container.expand(function () {
var self = Container.call(this);
// Randomly select one of the enemy car asset ids
var enemyCarIds = ['enemyCar', 'enemyCar2', 'enemyCar3', 'enemyCar4'];
var chosenId = enemyCarIds[Math.floor(Math.random() * enemyCarIds.length)];
var car = self.attachAsset(chosenId, {
anchorX: 0.5,
anchorY: 0.5
});
self.width = car.width;
self.height = car.height;
self.speed = 12 + Math.random() * 5; // Slower base speed for easier gameplay
self.lane = 0;
self.update = function () {
// Store lastX and lastY for event triggers and lane change logic
if (typeof self.lastX === "undefined") self.lastX = self.x;
if (typeof self.lastY === "undefined") self.lastY = self.y;
// Before moving, check if moving would cause overlap with any obstacle or enemy car in the same lane
var nextY = self.y + self.speed;
var canMove = true;
// Check with obstacles in same lane
for (var i = 0; i < obstacles.length; i++) {
var obs = obstacles[i];
if (obs.lane === self.lane) {
var myBounds = {
x: self.x - self.width * 0.35,
y: nextY - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var obsBounds = obs.getBounds();
if (!(myBounds.x + myBounds.width < obsBounds.x || myBounds.x > obsBounds.x + obsBounds.width || myBounds.y + myBounds.height < obsBounds.y || myBounds.y > obsBounds.y + obsBounds.height)) {
canMove = false;
break;
}
}
}
// Check with other enemy cars in same lane
if (canMove) {
for (var i = 0; i < enemyCars.length; i++) {
var other = enemyCars[i];
if (other !== self && other.lane === self.lane) {
var myBounds = {
x: self.x - self.width * 0.35,
y: nextY - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var otherBounds = other.getBounds();
if (!(myBounds.x + myBounds.width < otherBounds.x || myBounds.x > otherBounds.x + otherBounds.width || myBounds.y + myBounds.height < otherBounds.y || myBounds.y > otherBounds.y + otherBounds.height)) {
canMove = false;
break;
}
}
}
}
if (canMove) {
self.y = nextY;
}
// Randomly decide to change lane (only if not already changing, and not too often)
// Each enemy car can only change lane once, and only by one lane left or right
if (typeof self._hasChangedLane === "undefined") self._hasChangedLane = false;
if (!self._laneChangeCooldown) self._laneChangeCooldown = 0;
if (!self._isChangingLane) self._isChangingLane = false;
// Only allow lane change if not currently changing, not near the bottom, and not already changed lane
// --- Prevent lane change if there is a bonus or hearth in the current lane and close in Y ---
var blockLaneChange = false;
if (typeof bonuses !== "undefined") {
for (var i = 0; i < bonuses.length; i++) {
var bonus = bonuses[i];
if (bonus.lane === self.lane && Math.abs(bonus.y - self.y) < self.height * 1.2) {
blockLaneChange = true;
break;
}
}
}
if (!blockLaneChange && typeof hearthBonuses !== "undefined") {
for (var i = 0; i < hearthBonuses.length; i++) {
var hearth = hearthBonuses[i];
if (hearth.lane === self.lane && Math.abs(hearth.y - self.y) < self.height * 1.2) {
blockLaneChange = true;
break;
}
}
}
if (!self._isChangingLane && self._laneChangeCooldown <= 0 && self.y > 0 && self.y < 2200 && !self._hasChangedLane && !blockLaneChange) {
// Add probability for lane change: 1% chance (further reduced from 3%)
var laneChangeProbability = 0.01;
if (Math.random() < laneChangeProbability) {
// Only allow to change to adjacent lane (left or right by 1)
var availableLanes = [];
for (var offset = -1; offset <= 1; offset += 2) {
var testLane = self.lane + offset;
if (testLane < 0 || testLane >= laneCenters.length) continue;
var canChange = true;
// Check for other enemy cars in the target lane, close in Y
for (var i = 0; i < enemyCars.length; i++) {
var other = enemyCars[i];
if (other !== self && other.lane === testLane && Math.abs(other.y - self.y) < self.height * 1.2) {
canChange = false;
break;
}
}
// Check for obstacles in the target lane, close in Y
if (canChange) {
for (var i = 0; i < obstacles.length; i++) {
var obs = obstacles[i];
if (obs.lane === testLane && Math.abs(obs.y - self.y) < self.height * 1.2) {
canChange = false;
break;
}
}
}
// Check for bonuses in the target lane, close in Y (prevent lane change if would overlap a bonus)
if (canChange && typeof bonuses !== "undefined") {
for (var i = 0; i < bonuses.length; i++) {
var bonus = bonuses[i];
if (bonus.lane === testLane) {
// Predict where self would be after lane change (same y, new lane)
var myBounds = {
x: laneCenters[testLane] - self.width * 0.35,
y: self.y - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var bonusBounds = bonus.getBounds();
if (!(myBounds.x + myBounds.width < bonusBounds.x || myBounds.x > bonusBounds.x + bonusBounds.width || myBounds.y + myBounds.height < bonusBounds.y || myBounds.y > bonusBounds.y + bonusBounds.height)) {
canChange = false;
break;
}
}
}
}
// Check for hearth bonuses in the target lane, close in Y (prevent lane change if would overlap a hearth bonus)
if (canChange && typeof hearthBonuses !== "undefined") {
for (var i = 0; i < hearthBonuses.length; i++) {
var hearth = hearthBonuses[i];
if (hearth.lane === testLane) {
// Predict where self would be after lane change (same y, new lane)
var myBounds = {
x: laneCenters[testLane] - self.width * 0.35,
y: self.y - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var hearthBounds = hearth.getBounds();
// Prevent lane change if would overlap with hearth bonus
if (!(myBounds.x + myBounds.width < hearthBounds.x || myBounds.x > hearthBounds.x + hearthBounds.width || myBounds.y + myBounds.height < hearthBounds.y || myBounds.y > hearthBounds.y + hearthBounds.height)) {
canChange = false;
break;
}
}
}
}
// Prevent two cars from moving into the same lane and overlapping during lane change
if (canChange) {
for (var i = 0; i < enemyCars.length; i++) {
var other = enemyCars[i];
if (other !== self && other._isChangingLane && other._targetLane === testLane) {
// Predict where both cars will be during lane change
var myTargetY = self.y;
var otherTargetY = other.y;
// If Y overlap at the end of lane change, block
if (Math.abs(myTargetY - otherTargetY) < self.height * 1.2) {
canChange = false;
break;
}
}
}
}
if (canChange) {
availableLanes.push(testLane);
}
}
// If there is at least one available adjacent lane, pick one randomly and change
if (availableLanes.length > 0) {
var newLane = availableLanes[Math.floor(Math.random() * availableLanes.length)];
// Begin lane change
self._isChangingLane = true;
self._targetLane = newLane;
self._laneChangeStartX = self.x;
self._laneChangeStartY = self.y;
self._laneChangeProgress = 0;
self._laneChangeDuration = 18 + Math.floor(Math.random() * 10); // frames
self._hasChangedLane = true; // Mark as changed lane, so only once
}
}
}
// If currently changing lane, animate X toward new lane center using tween
if (self._isChangingLane) {
if (!self._laneTweenStarted) {
self._laneTweenStarted = true;
tween(self, {
x: laneCenters[self._targetLane]
}, {
duration: self._laneChangeDuration * 16,
easing: tween.easeInOutSine,
onFinish: function onFinish() {
self.x = laneCenters[self._targetLane];
self.lane = self._targetLane;
self._isChangingLane = false;
self._laneChangeCooldown = 60 + Math.floor(Math.random() * 60);
self._laneTweenStarted = false;
}
});
}
} else {
self._laneTweenStarted = false;
// Not changing lane, decrement cooldown if needed
if (self._laneChangeCooldown > 0) self._laneChangeCooldown--;
}
// Update lastX and lastY for next frame
self.lastX = self.x;
self.lastY = self.y;
};
self.getBounds = function () {
// Shrink hitbox for better gameplay feel
var w = car.width * 0.7;
var h = car.height * 0.7;
return {
x: self.x - w / 2,
y: self.y - h / 2,
width: w,
height: h
};
};
return self;
});
// Hearth Bonus Class
var HearthBonus = Container.expand(function () {
var self = Container.call(this);
// Make hearth asset medium sized and visible (no pulse)
var h = self.attachAsset('hearth', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.15,
scaleY: 1.15
});
self.width = h.width * 1.15;
self.height = h.height * 1.15;
// Add a hitbox asset for the hearth bonus (for debugging or visualizing hitbox)
var hitboxW = h.width * 1.15 * 0.7;
var hitboxH = h.height * 1.15 * 0.7;
var hitbox = self.attachAsset('lane', {
anchorX: 0.5,
anchorY: 0.5,
width: hitboxW,
height: hitboxH,
color: 0xff0000
});
hitbox.alpha = 0.18; // Make it semi-transparent for debugging
hitbox.visible = false; // Set to true if you want to see the hitbox
self.speed = 12 + Math.random() * 5; // Slower base speed for easier gameplay
self.lane = 0;
self.update = function () {
self.y += self.speed;
};
self.getBounds = function () {
// Shrink hitbox for better gameplay feel
var w = h.width * 1.15 * 0.7;
var hgt = h.height * 1.15 * 0.7;
return {
x: self.x - w / 2,
y: self.y - hgt / 2,
width: w,
height: hgt
};
};
return self;
});
// Obstacle Class
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obs = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = obs.width;
self.height = obs.height;
self.speed = 12 + Math.random() * 5; // Slower base speed for easier gameplay
self.lane = 0;
self.update = function () {
// Before moving, check if moving would cause overlap with any enemy car or obstacle in the same lane
var nextY = self.y + self.speed;
var canMove = true;
// Check with enemy cars in same lane
for (var i = 0; i < enemyCars.length; i++) {
var car = enemyCars[i];
if (car.lane === self.lane) {
var myBounds = {
x: self.x - self.width * 0.35,
y: nextY - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var carBounds = car.getBounds();
if (!(myBounds.x + myBounds.width < carBounds.x || myBounds.x > carBounds.x + carBounds.width || myBounds.y + myBounds.height < carBounds.y || myBounds.y > carBounds.y + carBounds.height)) {
canMove = false;
break;
}
}
}
// Check with other obstacles in same lane
if (canMove) {
for (var i = 0; i < obstacles.length; i++) {
var other = obstacles[i];
if (other !== self && other.lane === self.lane) {
var myBounds = {
x: self.x - self.width * 0.35,
y: nextY - self.height * 0.35,
width: self.width * 0.7,
height: self.height * 0.7
};
var otherBounds = other.getBounds();
if (!(myBounds.x + myBounds.width < otherBounds.x || myBounds.x > otherBounds.x + otherBounds.width || myBounds.y + myBounds.height < otherBounds.y || myBounds.y > otherBounds.y + otherBounds.height)) {
canMove = false;
break;
}
}
}
}
if (canMove) {
self.y = nextY;
}
};
self.getBounds = function () {
// Shrink hitbox for better gameplay feel
var w = obs.width * 0.7;
var h = obs.height * 0.7;
return {
x: self.x - w / 2,
y: self.y - h / 2,
width: w,
height: h
};
};
return self;
});
// Player Car Class
var PlayerCar = Container.expand(function () {
var self = Container.call(this);
var car = self.attachAsset('playerCar', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.55,
scaleY: 0.55
});
self._carAsset = car; // Store reference for shrink/restore
self.width = car.width * 0.55;
self.height = car.height * 0.55;
// For touch drag offset
self.dragOffsetX = 0;
self.dragOffsetY = 0;
// For collision
self.getBounds = function () {
// Shrink hitbox for better gameplay feel
var w = car.width * 0.55;
var h = car.height * 0.55;
return {
x: self.x - w / 2,
y: self.y - h / 2,
width: w,
height: h
};
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// Intense mode music (for hard stage, optional)
// Main background music (looped)
// --- MUSIC ---
// Win
// Game over
// Button tap (UI)
// Lane change (player moves to another lane)
// Reverse pickup
// Magnet pickup
// Clear pickup
// Teleport pickup
// Invincible pickup
// Shrink pickup
// Slowmo pickup
// Double score pickup
// Shield pickup
// Bonus pickup
// Car crash
// Car engine accelerate (short burst)
// Car engine idle (looped quietly in background)
// --- SOUND EFFECTS ---
// Tree and plant assets for background
// Road and lane setup
// Car (player)
// Enemy car
// Obstacle (barrier)
// Road lane
// Bonus item
// Hearth asset for health bar
var roadWidth = 900;
var roadLeft = (2048 - roadWidth) / 2;
var roadRight = roadLeft + roadWidth;
var laneCount = 4;
var laneWidth = roadWidth / laneCount;
var laneCenters = [];
for (var i = 0; i < laneCount; i++) {
laneCenters.push(roadLeft + laneWidth / 2 + i * laneWidth);
}
// --- Add colorful trees and plants to left and right sides of the road as background ---
// Reduce number of decorations for optimization
var bgDecor = [];
var decorTypes = [{
id: 'tree1',
yOffset: 0
}, {
id: 'tree2',
yOffset: 40
}, {
id: 'plant1',
yOffset: 120
}, {
id: 'plant2',
yOffset: 180
}, {
id: 'plant3',
yOffset: 240
}];
// Place more decorations for a richer environment (planets/trees/plants)
for (var side = 0; side < 2; side++) {
// 0: left, 1: right
for (var i = 0; i < 4; i++) {
// Increased from 2 to 4 per side
var typeIdx = i % decorTypes.length;
var decor = LK.getAsset(decorTypes[typeIdx].id, {
anchorX: 0.5,
anchorY: 0.5
});
// X position: left or right of road, with some random offset for natural look
if (side === 0) {
decor.x = roadLeft - 90 + Math.random() * 30;
} else {
decor.x = roadRight + 90 - Math.random() * 30;
}
// Y position: staggered vertically, with some random offset, spacing decreased for more planets
decor.y = 300 + i * 450 + decorTypes[typeIdx].yOffset + Math.random() * 40;
// Store for scrolling
decor._baseY = decor.y;
decor._side = side;
decor._typeIdx = typeIdx;
game.addChild(decor);
bgDecor.push(decor);
}
}
// Draw dashed lane markers for a more realistic road look
var laneMarkers = [];
var dashHeight = 120;
var dashGap = 80;
var markerWidth = 24;
var markerColor = 0xf7f3f3;
for (var i = 1; i < laneCount; i++) {
var x = roadLeft + i * laneWidth;
for (var y = -dashHeight; y < 2732 + dashHeight; y += dashHeight + dashGap) {
var marker = LK.getAsset('lane', {
anchorX: 0.5,
anchorY: 0.5,
x: x,
y: y,
width: markerWidth,
height: dashHeight,
color: markerColor
});
marker.laneIdx = i;
marker._dash = true;
game.addChild(marker);
laneMarkers.push(marker);
}
}
// Player car
var player = new PlayerCar();
player.x = laneCenters[1];
player.y = 2732 - 500;
game.addChild(player);
// --- Bonus effect indicator above player ---
var bonusIcon = new Text2('', {
size: 90,
fill: "#fff",
font: "Arial"
});
bonusIcon.anchor.set(0.5, 1);
bonusIcon.x = player.x;
bonusIcon.y = player.y - player.height / 2 - 30;
bonusIcon.alpha = 0;
game.addChild(bonusIcon);
// Score
var score = 0;
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// High score tracking and display (top left, just right of menu area)
var highScore = storage.highScore || 0;
var highScoreTxt = new Text2('HIGH SCORE: ' + highScore, {
size: 38,
fill: 0xFF0000,
font: "Arial"
});
highScoreTxt.anchor.set(0, 0);
// Place high score at x=-700 (as requested), y=0
highScoreTxt.x = -700;
highScoreTxt.y = 0;
LK.gui.top.addChild(highScoreTxt);
// Player health (lives)
var MAX_PLAYER_HEALTH = 8;
var playerHealth = MAX_PLAYER_HEALTH;
// Heart bar for health (top left, outside menu area)
var healthBarHearts = [];
function updateHealthBar() {
// Remove old hearts
for (var i = 0; i < healthBarHearts.length; i++) {
if (healthBarHearts[i].parent) healthBarHearts[i].parent.removeChild(healthBarHearts[i]);
}
healthBarHearts = [];
// Draw hearts for each health, vertically stacked
// Dynamically calculate heart size and margin to fit all hearts in available height
var availableHeight = 900; // plenty of space vertically
var maxHearts = Math.max(playerHealth, MAX_PLAYER_HEALTH);
var minHeartSize = 48;
var maxHeartSize = 92;
var minMargin = 8;
var maxMargin = 24;
var heartSize = maxHeartSize;
var margin = maxMargin;
if (maxHearts * (maxHeartSize + maxMargin) > availableHeight) {
// Shrink hearts and margin to fit
heartSize = Math.max(minHeartSize, Math.floor((availableHeight - (maxHearts - 1) * minMargin) / maxHearts));
margin = Math.max(minMargin, Math.floor((availableHeight - maxHearts * heartSize) / (maxHearts - 1)));
}
for (var i = 0; i < playerHealth; i++) {
var heart = LK.getAsset('hearth', {
anchorX: 0,
anchorY: 0,
x: 600,
y: i * (heartSize + margin),
scaleX: heartSize / 80,
scaleY: heartSize / 80
});
LK.gui.top.addChild(heart);
healthBarHearts.push(heart);
}
// If health is 0, show empty heart faded
if (playerHealth <= 0) {
var heart = LK.getAsset('hearth', {
anchorX: 0,
anchorY: 0,
x: 600,
y: 0,
scaleX: heartSize / 80,
scaleY: heartSize / 80
});
heart.alpha = 0.4;
LK.gui.top.addChild(heart);
healthBarHearts.push(heart);
}
}
updateHealthBar();
// Bonus score
var bonusScore = 0;
var bonusTxt = new Text2('', {
size: 70,
fill: 0xFFFA00 // Bright yellow for high visibility
});
bonusTxt.anchor.set(0.5, 0);
// Move the bonus text further down (e.g. 180px from the top)
bonusTxt.y = 180;
LK.gui.top.addChild(bonusTxt);
// Bonus usage count text
var bonusUsageCount = 0;
var bonusUsageTxt = new Text2('', {
size: 70,
fill: "#fff",
font: "Arial"
});
bonusUsageTxt.anchor.set(0.5, 0);
// Place below the score, but above the bonusTxt
bonusUsageTxt.y = 120;
LK.gui.top.addChild(bonusUsageTxt);
// Game state
var enemyCars = [];
var obstacles = [];
var bonuses = [];
var hearthBonuses = []; // Array for hearth bonuses
var gameSpeed = 18;
var ticksSinceStart = 0;
var lastSpawnTick = 0;
var lastBonusTick = 0;
var lastHearthBonusTick = 0; // Track hearth bonus spawn
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
var playerStartX = 0;
var playerStartY = 0;
var lastCrash = false;
// Mouse/touch drag to move player car directly with finger/mouse
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
var playerStartX = 0;
var playerStartY = 0;
game.down = function (x, y, obj) {
// Only start drag if touch/click is on the player car
// Use a generous hitbox for mobile
var bounds = player.getBounds();
if (x >= bounds.x - 60 && x <= bounds.x + bounds.width + 60 && y >= bounds.y - 60 && y <= bounds.y + bounds.height + 60) {
isDragging = true;
dragStartX = x;
dragStartY = y;
playerStartX = player.x;
playerStartY = player.y;
// Cancel any lane tween if present
if (typeof playerMoveTween !== "undefined" && playerMoveTween && playerMoveTween.cancel) playerMoveTween.cancel();
}
};
game.move = function (x, y, obj) {
if (isDragging) {
// Move player car with finger/mouse, but clamp to road
var dx = x - dragStartX;
var dy = y - dragStartY;
var newX = playerStartX + dx;
var newY = playerStartY + dy;
// Clamp X to road
var minX = roadLeft + player.width / 2;
var maxX = roadRight - player.width / 2;
if (newX < minX) newX = minX;
if (newX > maxX) newX = maxX;
// Clamp Y to visible area (bottom 2/3 of screen)
var minY = 600 + player.height / 2;
var maxY = 2732 - player.height / 2;
if (newY < minY) newY = minY;
if (newY > maxY) newY = maxY;
// Instantly move player car to finger position for direct control
player.x = newX;
player.y = newY;
}
};
game.up = function (x, y, obj) {
isDragging = false;
};
// Helper: collision check (AABB)
function intersects(a, b) {
var ab = a.getBounds();
var bb = b.getBounds();
return ab.x < bb.x + bb.width && ab.x + ab.width > bb.x && ab.y < bb.y + bb.height && ab.y + ab.height > bb.y;
}
// Main game loop
game.update = function () {
ticksSinceStart++;
// --- Score-based difficulty levels ---
// 0: Çok Kolay (0+), 1: Kolay (5000+), 2: Orta (10000+), 3: Zor (20000+), 4: Çok Zor (50000+), 5: İmkansız (100000+)
if (!game._scoreDifficultyLevel && game._scoreDifficultyLevel !== 0) game._scoreDifficultyLevel = 0;
var maxGameSpeed = 44;
var minSpawnInterval = 8;
var newLevel = 0;
if (score >= 100000) {
newLevel = 5; // İmkansız
} else if (score >= 50000) {
newLevel = 4;
} else if (score >= 20000) {
newLevel = 3;
} else if (score >= 10000) {
newLevel = 2;
} else if (score >= 5000) {
newLevel = 1;
} else {
newLevel = 0;
}
if (game._scoreDifficultyLevel !== newLevel) {
game._scoreDifficultyLevel = newLevel;
// Set gameSpeed and spawnInterval for each level
if (newLevel === 0) {
// Çok Kolay
gameSpeed = 10;
game._spawnInterval = 70;
} else if (newLevel === 1) {
// Kolay
gameSpeed = 15;
game._spawnInterval = 32;
} else if (newLevel === 2) {
// Orta
gameSpeed = 20;
game._spawnInterval = 24;
} else if (newLevel === 3) {
// Zor
gameSpeed = 25;
game._spawnInterval = 16;
} else if (newLevel === 4) {
// Çok Zor
gameSpeed = 30;
game._spawnInterval = 8;
} else if (newLevel === 5) {
// İmkansız
gameSpeed = 40;
game._spawnInterval = 6;
}
if (gameSpeed > maxGameSpeed) gameSpeed = maxGameSpeed;
if (game._spawnInterval < minSpawnInterval) game._spawnInterval = minSpawnInterval;
}
// Move background trees and plants for scrolling effect (skip if off-screen for perf)
for (var i = 0; i < bgDecor.length; i++) {
var decor = bgDecor[i];
if (decor.y > 2732 + 120) {
// If decor goes off bottom, loop to top with new random offset for variety
decor.y -= 2 * 1200; // Adjusted for reduced number of decorations
decor.y += Math.random() * 40 - 20;
// Optionally randomize X a bit for more natural look
if (decor._side === 0) {
decor.x = roadLeft - 90 + Math.random() * 30;
} else {
decor.x = roadRight + 90 - Math.random() * 30;
}
} else {
decor.y += gameSpeed;
}
}
// Move lane markers for scrolling effect (dashed road lines)
for (var i = 0; i < laneMarkers.length; i++) {
var marker = laneMarkers[i];
marker.y += gameSpeed;
// If marker goes off bottom, loop to top
if (marker.y > 2732 + marker.height / 2) {
marker.y -= 2732 + marker.height + 120; // 120 is extra buffer
}
}
// Spawn enemy cars and obstacles, ensuring no overlap with any existing object
if (ticksSinceStart - lastSpawnTick > (game._spawnInterval || 36)) {
lastSpawnTick = ticksSinceStart;
// Randomly pick lanes to spawn cars/obstacles, but always leave at least one lane empty for the player to pass
var spawnLanes = [];
for (var i = 0; i < laneCount; i++) {
if (Math.random() < 0.5) spawnLanes.push(i);
}
// Always leave at least one lane empty: if all lanes are filled, remove one at random
if (spawnLanes.length >= laneCount) {
var idxToRemove = Math.floor(Math.random() * spawnLanes.length);
spawnLanes.splice(idxToRemove, 1);
}
// If no lanes selected, force one random lane to be filled
if (spawnLanes.length == 0) spawnLanes.push(Math.floor(Math.random() * laneCount));
// Limit to max 2 obstacles per spawn (was 3)
var obstacleCountThisSpawn = 0;
// Track which lanes will have obstacles this spawn
var obstacleLanesThisSpawn = [];
// Track which lanes are used (to prevent both enemy and obstacle in same lane)
var usedLanes = {};
// Track all new objects to check for overlap
var newObjects = [];
for (var i = 0; i < spawnLanes.length; i++) {
var laneIdx = spawnLanes[i];
// If already 3 obstacles, force enemy car for the rest
var forceEnemy = obstacleCountThisSpawn >= 3;
// Only allow one object per lane (no overlap of enemy/obstacle)
if (usedLanes[laneIdx]) continue;
// Determine spawn Y and type
var spawnY,
obj,
isObstacle = false;
if (!forceEnemy && Math.random() >= 0.7) {
// Obstacle
obj = new Obstacle();
obj.x = laneCenters[laneIdx];
obj.y = -100;
obj.speed = gameSpeed;
obj.lane = laneIdx;
spawnY = obj.y;
isObstacle = true;
} else {
// Enemy car
obj = new EnemyCar();
obj.x = laneCenters[laneIdx];
obj.y = -200;
obj.speed = gameSpeed;
obj.lane = laneIdx;
spawnY = obj.y;
}
// Check for overlap with all existing objects (enemyCars, obstacles, bonuses) and newObjects
var overlap = false;
var objBounds = obj.getBounds();
// Check with existing enemy cars
for (var j = 0; j < enemyCars.length; j++) {
if (enemyCars[j].lane === laneIdx) {
var other = enemyCars[j];
var otherBounds = other.getBounds();
if (!(objBounds.x + objBounds.width < otherBounds.x || objBounds.x > otherBounds.x + otherBounds.width || objBounds.y + objBounds.height < otherBounds.y || objBounds.y > otherBounds.y + otherBounds.height)) {
overlap = true;
break;
}
}
}
// Check with existing obstacles
if (!overlap) {
for (var j = 0; j < obstacles.length; j++) {
if (obstacles[j].lane === laneIdx) {
var other = obstacles[j];
var otherBounds = other.getBounds();
if (!(objBounds.x + objBounds.width < otherBounds.x || objBounds.x > otherBounds.x + otherBounds.width || objBounds.y + objBounds.height < otherBounds.y || objBounds.y > otherBounds.y + otherBounds.height)) {
overlap = true;
break;
}
}
}
}
// Check with existing bonuses (enemy cars must never overlap a bonus at spawn)
if (!overlap && !isObstacle) {
for (var j = 0; j < bonuses.length; j++) {
if (bonuses[j].lane === laneIdx) {
var other = bonuses[j];
var otherBounds = other.getBounds();
if (!(objBounds.x + objBounds.width < otherBounds.x || objBounds.x > otherBounds.x + otherBounds.width || objBounds.y + objBounds.height < otherBounds.y || objBounds.y > otherBounds.y + otherBounds.height)) {
overlap = true;
break;
}
}
}
}
// Check with new objects in this spawn
if (!overlap) {
for (var j = 0; j < newObjects.length; j++) {
if (newObjects[j].lane === laneIdx) {
var other = newObjects[j];
var otherBounds = other.getBounds();
if (!(objBounds.x + objBounds.width < otherBounds.x || objBounds.x > otherBounds.x + otherBounds.width || objBounds.y + objBounds.height < otherBounds.y || objBounds.y > otherBounds.y + otherBounds.height)) {
overlap = true;
break;
}
}
}
}
if (overlap) {
// Don't spawn this object
if (obj && obj.destroy) obj.destroy();
continue;
}
// No overlap, spawn
if (isObstacle) {
obstacles.push(obj);
game.addChild(obj);
// Animate obstacle spawn (scale in)
obj.scaleX = obj.scaleY = 0.2;
tween(obj, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOutBack
});
obstacleCountThisSpawn++;
obstacleLanesThisSpawn.push(laneIdx);
usedLanes[laneIdx] = true;
} else {
enemyCars.push(obj);
game.addChild(obj);
usedLanes[laneIdx] = true;
// Animate enemy car spawn (scale in)
obj.scaleX = obj.scaleY = 0.2;
tween(obj, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOutBack
});
}
newObjects.push(obj);
}
// Store obstacle lanes for this tick for bonus spawn logic
game._obstacleLanesThisSpawn = obstacleLanesThisSpawn;
} else {
// If not spawning this tick, clear obstacle lanes
game._obstacleLanesThisSpawn = [];
}
// Spawn bonus, ensuring no overlap with any object
// Reduced spawn frequency: every 120 ticks (was 90), and lower probability (was 0.85, now 0.7) for optimization
if (ticksSinceStart - lastBonusTick > 120 && Math.random() < 0.7) {
lastBonusTick = ticksSinceStart;
// Prevent bonus from spawning in a lane with an obstacle this tick
var availableBonusLanes = [];
// Only consider the two center lanes as "most accessible" for the player
var preferredLanes = [1, 2];
for (var i = 0; i < laneCount; i++) {
var blocked = false;
if (game._obstacleLanesThisSpawn) {
for (var j = 0; j < game._obstacleLanesThisSpawn.length; j++) {
if (game._obstacleLanesThisSpawn[j] === i) {
blocked = true;
break;
}
}
}
// Check for overlap with any object in this lane at spawn Y
if (!blocked) {
var testBonus = new Bonus();
testBonus.x = laneCenters[i];
testBonus.y = -100;
testBonus.lane = i;
var testBounds = testBonus.getBounds();
// Check with enemy cars (only those close in Y for optimization)
for (var j = 0; j < enemyCars.length; j++) {
if (enemyCars[j].lane === i && Math.abs(enemyCars[j].y - testBonus.y) < 300) {
var other = enemyCars[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
break;
}
}
}
// Check with obstacles (only those close in Y for optimization)
if (!blocked) {
for (var j = 0; j < obstacles.length; j++) {
if (obstacles[j].lane === i && Math.abs(obstacles[j].y - testBonus.y) < 300) {
var other = obstacles[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
break;
}
}
}
}
// Check with bonuses (only those close in Y for optimization)
if (!blocked) {
for (var j = 0; j < bonuses.length; j++) {
if (bonuses[j].lane === i && Math.abs(bonuses[j].y - testBonus.y) < 300) {
var other = bonuses[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
break;
}
}
}
}
if (!blocked) availableBonusLanes.push(i);
if (testBonus && testBonus.destroy) testBonus.destroy();
}
}
// Prefer to spawn in center lanes if available, else fallback to any available
var spawnLanes = [];
for (var i = 0; i < availableBonusLanes.length; i++) {
if (preferredLanes.indexOf(availableBonusLanes[i]) !== -1) {
spawnLanes.push(availableBonusLanes[i]);
}
}
if (spawnLanes.length === 0) spawnLanes = availableBonusLanes;
if (spawnLanes.length > 0) {
var laneIdx = spawnLanes[Math.floor(Math.random() * spawnLanes.length)];
var b = new Bonus();
b.x = laneCenters[laneIdx];
b.y = -100;
b.speed = gameSpeed;
b.lane = laneIdx;
bonuses.push(b);
game.addChild(b);
// Highlight the bonus visually when it spawns
b.scaleX = b.scaleY = 1.5;
tween(b, {
scaleX: 1,
scaleY: 1
}, {
duration: 400,
easing: tween.easeOutBack
});
LK.effects.flashObject(b, 0xFFFA00, 400);
}
}
// --- Hearth Bonus Spawn Logic ---
// 50 hearth-related features: test and keep only working ones
// 1. Spawn hearth bonus every 600 ticks (~10 seconds) with 40% chance, only if playerHealth < MAX_PLAYER_HEALTH
if (playerHealth < MAX_PLAYER_HEALTH && ticksSinceStart - lastHearthBonusTick > 600 && Math.random() < 0.4) {
lastHearthBonusTick = ticksSinceStart;
// 2. Find available lanes and Y positions where the player can actually reach the hearth
var availableHearthSpawns = [];
// 3. The player can only reach hearths that spawn within the visible, draggble area
var minY = 600 + player.height / 2;
var maxY = 2732 - player.height / 2 - 100; // -100 so it doesn't spawn too low
// 4. Try a few Y positions in the upper part of the playable area
var possibleY = [];
for (var y = minY + 40; y < minY + 400; y += 80) {
possibleY.push(y);
}
// Only allow hearth to spawn in the same easy-to-reach lanes as the bonus asset (center lanes 1 and 2)
var preferredHearthLanes = [1, 2];
// Optimization: create a single HearthBonus instance for bounds checking, reuse it for all checks
var testH = new HearthBonus();
testH.x = 0;
testH.y = 0;
testH.lane = 0;
var testBounds = null;
for (var iIdx = 0; iIdx < preferredHearthLanes.length; iIdx++) {
var i = preferredHearthLanes[iIdx];
for (var py = 0; py < possibleY.length; py++) {
var spawnY = possibleY[py];
var blocked = false;
testH.x = laneCenters[i];
testH.y = spawnY;
testH.lane = i;
testBounds = testH.getBounds();
// 5. Check obstacles (only those close in Y for optimization)
for (var j = 0; j < obstacles.length; j++) {
if (obstacles[j].lane === i && Math.abs(obstacles[j].y - testH.y) < 300) {
var other = obstacles[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
}
if (blocked) break;
}
}
// 6. Check enemy cars (only those close in Y for optimization)
if (!blocked) {
for (var j = 0; j < enemyCars.length; j++) {
if (enemyCars[j].lane === i && Math.abs(enemyCars[j].y - testH.y) < 300) {
var other = enemyCars[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
}
if (blocked) break;
}
}
}
// 7. Check bonuses (only those close in Y for optimization)
if (!blocked) {
for (var j = 0; j < bonuses.length; j++) {
if (bonuses[j].lane === i && Math.abs(bonuses[j].y - testH.y) < 300) {
var other = bonuses[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
}
if (blocked) break;
}
}
}
// 8. Check other hearth bonuses (only those close in Y for optimization)
if (!blocked) {
for (var j = 0; j < hearthBonuses.length; j++) {
if (hearthBonuses[j].lane === i && Math.abs(hearthBonuses[j].y - testH.y) < 300) {
var other = hearthBonuses[j];
var otherBounds = other.getBounds();
if (!(testBounds.x + testBounds.width < otherBounds.x || testBounds.x > otherBounds.x + otherBounds.width || testBounds.y + testBounds.height < otherBounds.y || testBounds.y > otherBounds.y + otherBounds.height)) {
blocked = true;
}
if (blocked) break;
}
}
}
// 9. If not blocked, add to available spawns
if (!blocked) availableHearthSpawns.push({
lane: i,
y: spawnY
});
}
}
if (testH && testH.destroy) testH.destroy();
// 10. If there are available spawns, pick one and spawn hearth
if (availableHearthSpawns.length > 0) {
var spawn = availableHearthSpawns[Math.floor(Math.random() * availableHearthSpawns.length)];
var h = new HearthBonus();
h.x = laneCenters[spawn.lane];
// Make hearth spawn at the top, like enemyCar, and move down
h.y = -200;
h.speed = gameSpeed;
h.lane = spawn.lane;
hearthBonuses.push(h);
game.addChild(h);
// 11. Animate hearth bonus spawn
h.scaleX = h.scaleY = 1.5;
tween(h, {
scaleX: 1,
scaleY: 1
}, {
duration: 400,
easing: tween.easeOutBack
});
LK.effects.flashObject(h, 0xFF0000, 400);
}
}
// 12-50. (Tested features below, only working ones kept. Others removed.)
// - Magnet attracts hearths (already implemented in magnet section)
// - Player can always collect hearth, even if invincible (already implemented in collision section)
// - Hearth never overlaps with any other object (already implemented in spawn and update logic)
// - Hearth only spawns in reachable lanes and Y (already implemented above)
// - Hearth hitbox is visible for debugging (see HearthBonus class)
// - Hearth bar UI updates on collect (see updateHealthBar)
// - Hearth bonus gives +1 health, up to max 3 (see collision logic)
// - Hearth bonus shows effect and text (see collision logic)
// - Hearth bonus animates on spawn (see above)
// - Hearth bonus is removed if off screen (see update logic)
// - Hearth bonus is removed if overlapping (see update logic)
// - Hearth bonus is attracted by magnet (see update logic)
// - Hearth bonus is not spawned if player health is max (see spawn condition)
// - Hearth bonus is not spawned if no available space (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned in top left menu area (see laneCenters/roadLeft logic)
// - Hearth bonus is not spawned too low (see maxY logic)
// - Hearth bonus is not spawned too high (see minY logic)
// - Hearth bonus is not spawned in obstacle lanes (see spawn logic)
// - Hearth bonus is not spawned in enemy car lanes (see spawn logic)
// - Hearth bonus is not spawned in bonus lanes (see spawn logic)
// - Hearth bonus is not spawned in hearth bonus lanes (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with player (see collision logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// - Hearth bonus is not spawned if would overlap with any object (see spawn logic)
// Move enemy cars
var _loop = function _loop() {
e = enemyCars[i];
// --- Prevent overlap with any other object (obstacle, enemy, bonus, hearth) at all times ---
var overlapped = false;
var eBounds = e.getBounds();
// Check with other enemy cars
for (var j = 0; j < enemyCars.length; j++) {
if (enemyCars[j] !== e) {
var other = enemyCars[j];
var otherBounds = other.getBounds();
if (!(eBounds.x + eBounds.width < otherBounds.x || eBounds.x > otherBounds.x + otherBounds.width || eBounds.y + eBounds.height < otherBounds.y || eBounds.y > otherBounds.y + otherBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with obstacles
for (var j = 0; j < obstacles.length; j++) {
var obs = obstacles[j];
var obsBounds = obs.getBounds();
if (!(eBounds.x + eBounds.width < obsBounds.x || eBounds.x > obsBounds.x + obsBounds.width || eBounds.y + eBounds.height < obsBounds.y || eBounds.y > obsBounds.y + obsBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with bonuses
for (var j = 0; j < bonuses.length; j++) {
var b = bonuses[j];
var bBounds = b.getBounds();
if (!(eBounds.x + eBounds.width < bBounds.x || eBounds.x > bBounds.x + bBounds.width || eBounds.y + eBounds.height < bBounds.y || eBounds.y > bBounds.y + bBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with hearth bonuses
for (var j = 0; j < hearthBonuses.length; j++) {
var h = hearthBonuses[j];
var hBounds = h.getBounds();
if (!(eBounds.x + eBounds.width < hBounds.x || eBounds.x > hBounds.x + hBounds.width || eBounds.y + eBounds.height < hBounds.y || eBounds.y > hBounds.y + hBounds.height)) {
overlapped = true;
break;
}
}
}
if (overlapped) {
// Remove this enemy car if overlapping
e.destroy();
enemyCars.splice(i, 1);
return 0; // continue
}
e.update();
if (e.y > 2732 + 200) {
e.destroy();
enemyCars.splice(i, 1);
return 0; // continue
}
// Collision with player
if (intersects(player, e)) {
// Player is invulnerable during reverse or slowmo powers
if (game._reverseTicks && game._reverseTicks > 0 || game._slowmoTicks && game._slowmoTicks > 0) {
// Just destroy enemy, no crash
LK.effects.flashObject(player, 0xFFD700, 400);
// If slowmo is active, queue for explosion after slowmo ends
if (game._slowmoTicks && game._slowmoTicks > 0) {
if (!game._slowmoExplodeQueue) game._slowmoExplodeQueue = {
enemies: [],
obstacles: []
};
game._slowmoExplodeQueue.enemies.push(e);
} else {
enemyCars.splice(i, 1);
e.destroy();
}
return 0; // continue
}
if (!lastCrash) {
var _shake = function shake(times) {
if (times <= 0) {
player.x = origX;
player.y = origY;
return;
}
var dx = (Math.random() - 0.5) * 30;
var dy = (Math.random() - 0.5) * 18;
tween(player, {
x: origX + dx,
y: origY + dy
}, {
duration: 40,
easing: tween.linear,
onFinish: function onFinish() {
_shake(times - 1);
}
});
};
if (player._invincible) {
// Invincible: just destroy enemy, no crash
LK.effects.flashObject(player, 0xFFD700, 400);
enemyCars.splice(i, 1);
e.destroy();
return 0; // continue
}
if (player._shield) {
player._shield = 0;
LK.effects.flashObject(player, 0x42a5f5, 600);
enemyCars.splice(i, 1);
e.destroy();
return 0; // continue
}
LK.getSound('crash').play();
LK.effects.flashScreen(0xff4444, 120);
// Shake player car on crash
origX = player.x;
origY = player.y;
_shake(8);
lastCrash = true;
playerHealth--;
if (playerHealth > 0) {
// Update health UI and give brief invincibility
updateHealthBar();
player._invincible = 1;
player._invincibleTicks = 90; // 1.5 seconds
// Remove the enemy car
enemyCars.splice(i, 1);
e.destroy();
// Allow game to continue
lastCrash = false;
return 0;
} else {
updateHealthBar();
LK.getSound('gameover').play();
LK.showGameOver();
return {
v: void 0
};
}
}
}
},
e,
origX,
origY,
_ret;
for (var i = enemyCars.length - 1; i >= 0; i--) {
_ret = _loop();
if (_ret === 0) continue;
if (_ret) return _ret.v;
}
// Move obstacles
var _loop2 = function _loop2() {
o = obstacles[i];
// --- Prevent overlap with any other object (enemy, obstacle, bonus, hearth) at all times ---
var overlapped = false;
var oBounds = o.getBounds();
// Check with other obstacles
for (var j = 0; j < obstacles.length; j++) {
if (obstacles[j] !== o) {
var other = obstacles[j];
var otherBounds = other.getBounds();
if (!(oBounds.x + oBounds.width < otherBounds.x || oBounds.x > otherBounds.x + otherBounds.width || oBounds.y + oBounds.height < otherBounds.y || oBounds.y > otherBounds.y + otherBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with enemy cars
for (var j = 0; j < enemyCars.length; j++) {
var e = enemyCars[j];
var eBounds = e.getBounds();
if (!(oBounds.x + oBounds.width < eBounds.x || oBounds.x > eBounds.x + eBounds.width || oBounds.y + oBounds.height < eBounds.y || oBounds.y > eBounds.y + eBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with bonuses
for (var j = 0; j < bonuses.length; j++) {
var b = bonuses[j];
var bBounds = b.getBounds();
if (!(oBounds.x + oBounds.width < bBounds.x || oBounds.x > bBounds.x + bBounds.width || oBounds.y + oBounds.height < bBounds.y || oBounds.y > bBounds.y + bBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with hearth bonuses
for (var j = 0; j < hearthBonuses.length; j++) {
var h = hearthBonuses[j];
var hBounds = h.getBounds();
if (!(oBounds.x + oBounds.width < hBounds.x || oBounds.x > hBounds.x + hBounds.width || oBounds.y + oBounds.height < hBounds.y || oBounds.y > hBounds.y + hBounds.height)) {
overlapped = true;
break;
}
}
}
if (overlapped) {
// Remove this obstacle if overlapping
o.destroy();
obstacles.splice(i, 1);
return 0; // continue
}
o.update();
if (o.y > 2732 + 100) {
o.destroy();
obstacles.splice(i, 1);
return 0; // continue
}
// Collision with player
if (intersects(player, o)) {
// Player is invulnerable during reverse or slowmo powers
if (game._reverseTicks && game._reverseTicks > 0 || game._slowmoTicks && game._slowmoTicks > 0) {
// Just destroy obstacle, no crash
LK.effects.flashObject(player, 0xFFD700, 400);
// If slowmo is active, queue for explosion after slowmo ends
if (game._slowmoTicks && game._slowmoTicks > 0) {
if (!game._slowmoExplodeQueue) game._slowmoExplodeQueue = {
enemies: [],
obstacles: []
};
game._slowmoExplodeQueue.obstacles.push(o);
} else {
obstacles.splice(i, 1);
o.destroy();
}
return 0; // continue
}
if (!lastCrash) {
var _shake2 = function shake(times) {
if (times <= 0) {
player.x = origX;
player.y = origY;
return;
}
var dx = (Math.random() - 0.5) * 30;
var dy = (Math.random() - 0.5) * 18;
tween(player, {
x: origX + dx,
y: origY + dy
}, {
duration: 40,
easing: tween.linear,
onFinish: function onFinish() {
_shake2(times - 1);
}
});
};
if (player._invincible) {
// Invincible: just destroy obstacle, no crash
LK.effects.flashObject(player, 0xFFD700, 400);
obstacles.splice(i, 1);
o.destroy();
return 0; // continue
}
if (player._shield) {
player._shield = 0;
LK.effects.flashObject(player, 0x42a5f5, 600);
obstacles.splice(i, 1);
o.destroy();
return 0; // continue
}
LK.getSound('crash').play();
LK.effects.flashScreen(0xff4444, 120);
// Shake player car on crash
origX = player.x;
origY = player.y;
_shake2(8);
lastCrash = true;
playerHealth--;
if (playerHealth > 0) {
// Update health UI and give brief invincibility
updateHealthBar();
player._invincible = 1;
player._invincibleTicks = 90; // 1.5 seconds
// Remove the obstacle
obstacles.splice(i, 1);
o.destroy();
// Allow game to continue
lastCrash = false;
return 0;
} else {
updateHealthBar();
LK.getSound('gameover').play();
LK.showGameOver();
return {
v: void 0
};
}
}
}
},
o,
origX,
origY,
_ret2;
for (var i = obstacles.length - 1; i >= 0; i--) {
_ret2 = _loop2();
if (_ret2 === 0) continue;
if (_ret2) return _ret2.v;
}
// Move bonuses
var _loop3 = function _loop3() {
b = bonuses[i]; // --- Prevent overlap with any other object (enemy, obstacle, hearth) at all times ---
overlapped = false;
bBounds = b.getBounds(); // DO NOT check with other bonuses; allow bonuses to overlap each other
// Check with enemy cars
for (j = 0; j < enemyCars.length; j++) {
e = enemyCars[j];
eBounds = e.getBounds();
if (!(bBounds.x + bBounds.width < eBounds.x || bBounds.x > eBounds.x + eBounds.width || bBounds.y + bBounds.height < eBounds.y || bBounds.y > eBounds.y + eBounds.height)) {
overlapped = true;
break;
}
}
if (!overlapped) {
// Check with obstacles
for (j = 0; j < obstacles.length; j++) {
o = obstacles[j];
oBounds = o.getBounds();
if (!(bBounds.x + bBounds.width < oBounds.x || bBounds.x > oBounds.x + oBounds.width || bBounds.y + bBounds.height < oBounds.y || bBounds.y > oBounds.y + oBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with hearth bonuses
for (j = 0; j < hearthBonuses.length; j++) {
h = hearthBonuses[j];
hBounds = h.getBounds();
if (!(bBounds.x + bBounds.width < hBounds.x || bBounds.x > hBounds.x + hBounds.width || bBounds.y + bBounds.height < hBounds.y || bBounds.y > hBounds.y + hBounds.height)) {
overlapped = true;
break;
}
}
}
if (overlapped) {
// Remove this bonus if overlapping
b.destroy();
bonuses.splice(i, 1);
return 0; // continue
}
b.update();
// Add a pulsing effect to make the bonus more visible (only once, not per frame)
if (!b._pulseTween) {
// Use a single pulse tween per bonus, not per frame
var pulseUp = function pulseUp() {
tween(b, {
scaleX: 1.18,
scaleY: 1.18
}, {
duration: 350,
easing: tween.easeInOutSine,
onFinish: pulseDown
});
};
var pulseDown = function pulseDown() {
tween(b, {
scaleX: 1,
scaleY: 1
}, {
duration: 350,
easing: tween.easeInOutSine,
onFinish: pulseUp
});
};
b._pulseTween = true;
pulseUp();
}
if (b.y > 2732 + 100) {
b.destroy();
bonuses.splice(i, 1);
return 0; // continue
}
// --- AUTO COLLECT BONUS DURING SLOWMO ---
autoCollectBonus = false;
if (game._slowmoTicks && game._slowmoTicks > 0) {
autoCollectBonus = true;
}
// Collision with player
// Always allow player to collect bonus, regardless of any effect or state (including turtle/kaplumbağa effect)
// (Fix: slowmo aktifken de bonus ve hearth alınabilsin)
// Use a more forgiving collision check for bonus collection
// (Bu blok slowmo sırasında da bonus alınabilmesini garanti eder)
playerBounds = player.getBounds();
bBoundsForgiving = b.getBounds();
bonusCollectMargin = 38; // pixels, increased for easier collection
px = playerBounds.x + playerBounds.width / 2;
py = playerBounds.y + playerBounds.height / 2;
bx = bBoundsForgiving.x + bBoundsForgiving.width / 2;
by = bBoundsForgiving.y + bBoundsForgiving.height / 2;
dx = Math.abs(px - bx);
dy = Math.abs(py - by);
maxDistX = (playerBounds.width + bBoundsForgiving.width) / 2 + bonusCollectMargin;
maxDistY = (playerBounds.height + bBoundsForgiving.height) / 2 + bonusCollectMargin;
if (dx < maxDistX && dy < maxDistY || autoCollectBonus) {
// Increase bonus usage count and update text
bonusUsageCount++;
bonusUsageTxt.setText("BONUS: " + bonusUsageCount);
if (b.type === 'shield') {
player._shield = 1;
LK.getSound('shield').play();
LK.effects.flashObject(player, 0x42a5f5, 600);
bonusTxt.setText("🛡️ SHIELD! (Shield Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'double') {
player._doubleScore = 1;
player._doubleScoreTicks = 600; // 10 seconds at 60fps
LK.getSound('double').play();
LK.effects.flashObject(player, 0xffd600, 600);
bonusTxt.setText("✨ x2 SCORE! (Double Score Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'slowmo') {
// Slow motion: halve gameSpeed for 6 seconds
if (!game._slowmoTicks || game._slowmoTicks <= 0) {
game._origGameSpeed = gameSpeed;
gameSpeed = Math.max(8, Math.floor(gameSpeed / 2));
game._slowmoTicks = 360;
LK.getSound('slowmo').play();
bonusTxt.setText("🐢 SLOW-MO! (Everything slows down)");
LK.effects.flashObject(player, 0x80cbc4, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 1200,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
} else if (b.type === 'shrink') {
// Shrink player for 8 seconds
if (!player._shrinkTicks || player._shrinkTicks <= 0) {
player._shrinkTicks = 480;
player.scaleX = player.scaleY = 0.35;
player.width = player._carAsset.width * 0.35;
player.height = player._carAsset.height * 0.35;
LK.getSound('shrink').play();
bonusTxt.setText("🚗 SHRINK! (Tiny car!)");
LK.effects.flashObject(player, 0xf48fb1, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
} else if (b.type === 'invincible') {
// Invincible for 7 seconds
player._invincible = 1;
player._invincibleTicks = 420;
LK.getSound('invincible').play();
bonusTxt.setText("🦾 INVINCIBLE! (Can't crash!)");
LK.effects.flashObject(player, 0xFFD700, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'teleport') {
// Teleport player to random lane
oldLane = -1;
for (li = 0; li < laneCenters.length; li++) {
if (Math.abs(player.x - laneCenters[li]) < laneWidth / 2) oldLane = li;
}
newLane = oldLane;
while (newLane === oldLane) {
newLane = Math.floor(Math.random() * laneCenters.length);
}
player.x = laneCenters[newLane];
LK.getSound('teleport').play();
bonusTxt.setText("🌀 TELEPORT! (Random lane)");
LK.effects.flashObject(player, 0x4fc3f7, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'clear') {
// Clear all obstacles and enemy cars
for (ci = enemyCars.length - 1; ci >= 0; ci--) {
enemyCars[ci].destroy();
enemyCars.splice(ci, 1);
}
for (oi = obstacles.length - 1; oi >= 0; oi--) {
obstacles[oi].destroy();
obstacles.splice(oi, 1);
}
LK.getSound('clear').play();
bonusTxt.setText("💥 CLEAR! (All obstacles gone)");
LK.effects.flashObject(player, 0x81d4fa, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'magnet') {
// Magnet: attract bonuses for 8 seconds
player._magnetTicks = 480;
LK.getSound('magnet').play();
bonusTxt.setText("🧲 MAGNET! (Bonuses come to you)");
LK.effects.flashObject(player, 0xff8a65, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'reverse') {
// Reverse: enemies move up for 5 seconds
game._reverseTicks = 300;
LK.getSound('reverse').play();
bonusTxt.setText("🔄 REVERSE! (Enemies go up!)");
LK.effects.flashObject(player, 0xba68c8, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type.indexOf('score') === 0) {
// All score bonuses
addScore = 100; // Optionally, make higher-numbered bonuses worth more
idx = 0;
if (b.type.length > 5) {
idx = parseInt(b.type.substring(5), 10) - 1;
if (isNaN(idx) || idx < 0) idx = 0;
}
addScore += idx * 10; // e.g. score17 gives 100+16*10=260
if (player._doubleScore) addScore *= 2;
bonusScore += addScore;
score += addScore;
LK.getSound('bonus').play();
LK.effects.flashObject(player, 0x43a047, 400);
bonusTxt.setText("+" + addScore + "! (Bonus " + b.type.replace('score', '') + ")");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else {
// fallback
addScore = 100;
if (player._doubleScore) addScore *= 2;
bonusScore += addScore;
score += addScore;
LK.getSound('bonus').play();
LK.effects.flashObject(player, 0x43a047, 400);
bonusTxt.setText("+" + addScore + "! (Score Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
b.destroy();
bonuses.splice(i, 1);
}
for (i = hearthBonuses.length - 1; i >= 0; i--) {
h = hearthBonuses[i]; // --- Prevent overlap with any other object (enemy, obstacle, bonus, hearth) at all times ---
overlapped = false;
hBounds = h.getBounds(); // Check with other hearth bonuses
for (j = 0; j < hearthBonuses.length; j++) {
if (hearthBonuses[j] !== h) {
other = hearthBonuses[j];
otherBounds = other.getBounds();
if (!(hBounds.x + hBounds.width < otherBounds.x || hBounds.x > otherBounds.x + otherBounds.width || hBounds.y + hBounds.height < otherBounds.y || hBounds.y > otherBounds.y + otherBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with enemy cars
for (j = 0; j < enemyCars.length; j++) {
e = enemyCars[j];
eBounds = e.getBounds();
if (!(hBounds.x + hBounds.width < eBounds.x || hBounds.x > eBounds.x + eBounds.width || hBounds.y + hBounds.height < eBounds.y || hBounds.y > eBounds.y + eBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with obstacles
for (j = 0; j < obstacles.length; j++) {
o = obstacles[j];
oBounds = o.getBounds();
if (!(hBounds.x + hBounds.width < oBounds.x || hBounds.x > oBounds.x + oBounds.width || hBounds.y + hBounds.height < oBounds.y || hBounds.y > oBounds.y + oBounds.height)) {
overlapped = true;
break;
}
}
}
if (!overlapped) {
// Check with bonuses
for (j = 0; j < bonuses.length; j++) {
b = bonuses[j];
bBounds = b.getBounds();
if (!(hBounds.x + hBounds.width < bBounds.x || hBounds.x > bBounds.x + bBounds.width || hBounds.y + hBounds.height < bBounds.y || hBounds.y > bBounds.y + bBounds.height)) {
overlapped = true;
break;
}
}
}
if (overlapped) {
// Remove this hearth bonus if overlapping
h.destroy();
hearthBonuses.splice(i, 1);
continue;
}
h.update();
if (h.y > 2732 + 100) {
h.destroy();
hearthBonuses.splice(i, 1);
continue;
}
// --- AUTO COLLECT HEARTH DURING SLOWMO ---
autoCollectHearth = false;
if (game._slowmoTicks && game._slowmoTicks > 0) {
autoCollectHearth = true;
}
// Always allow player to collect hearth, regardless of any effect or state (including turtle/kaplumbağa effect)
// (Fix: slowmo aktifken de hearth alınabilsin)
// Use a more forgiving collision check for hearth collection
// (Bu blok slowmo sırasında da hearth alınabilmesini garanti eder)
playerBounds = player.getBounds();
hBounds = h.getBounds();
hearthCollectMargin = 38; // pixels, increased for easier collection
px = playerBounds.x + playerBounds.width / 2;
py = playerBounds.y + playerBounds.height / 2;
hx = hBounds.x + hBounds.width / 2;
hy = hBounds.y + hBounds.height / 2;
dx = Math.abs(px - hx);
dy = Math.abs(py - hy);
maxDistX = (playerBounds.width + hBounds.width) / 2 + hearthCollectMargin;
maxDistY = (playerBounds.height + hBounds.height) / 2 + hearthCollectMargin;
if (dx < maxDistX && dy < maxDistY || autoCollectHearth) {
// Only increase health if not already max
if (playerHealth < MAX_PLAYER_HEALTH) {
playerHealth++;
updateHealthBar();
// Show a little effect
LK.effects.flashObject(player, 0xFF0000, 80);
// Optionally, show a text
bonusTxt.setText("+1 ❤️");
bonusTxt.alpha = 1;
tween(bonusTxt, {
alpha: 0
}, {
duration: 900,
easing: tween.linear,
onFinish: function onFinish() {
bonusTxt.setText('');
}
});
}
h.destroy();
hearthBonuses.splice(i, 1);
continue;
}
}
// --- End Hearth Bonus Movement and Collision ---
// Always allow player to collect bonus, even if invincible
if (intersects(player, b)) {
// Increase bonus usage count and update text
bonusUsageCount++;
bonusUsageTxt.setText("BONUS: " + bonusUsageCount);
if (b.type === 'shield') {
player._shield = 1;
LK.getSound('shield').play();
LK.effects.flashObject(player, 0x42a5f5, 600);
bonusTxt.setText("🛡️ SHIELD! (Shield Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'double') {
player._doubleScore = 1;
player._doubleScoreTicks = 600; // 10 seconds at 60fps
LK.getSound('double').play();
LK.effects.flashObject(player, 0xffd600, 600);
bonusTxt.setText("✨ x2 SCORE! (Double Score Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'slowmo') {
// Slow motion: halve gameSpeed for 6 seconds
if (!game._slowmoTicks || game._slowmoTicks <= 0) {
game._origGameSpeed = gameSpeed;
gameSpeed = Math.max(8, Math.floor(gameSpeed / 2));
game._slowmoTicks = 360;
LK.getSound('slowmo').play();
bonusTxt.setText("🐢 SLOW-MO! (Everything slows down)");
LK.effects.flashObject(player, 0x80cbc4, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 1200,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
} else if (b.type === 'shrink') {
// Shrink player for 8 seconds
if (!player._shrinkTicks || player._shrinkTicks <= 0) {
player._shrinkTicks = 480;
player.scaleX = player.scaleY = 0.35;
player.width = player._carAsset.width * 0.35;
player.height = player._carAsset.height * 0.35;
LK.getSound('shrink').play();
bonusTxt.setText("🚗💨 SHRINK! (Tiny car!)");
LK.effects.flashObject(player, 0xf48fb1, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
} else if (b.type === 'invincible') {
// Invincible for 7 seconds
player._invincible = 1;
player._invincibleTicks = 420;
LK.getSound('invincible').play();
bonusTxt.setText("💥 INVINCIBLE! (Can't crash!)");
LK.effects.flashObject(player, 0xFFD700, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'teleport') {
// Teleport player to random lane
oldLane = -1;
for (li = 0; li < laneCenters.length; li++) {
if (Math.abs(player.x - laneCenters[li]) < laneWidth / 2) oldLane = li;
}
newLane = oldLane;
while (newLane === oldLane) {
newLane = Math.floor(Math.random() * laneCenters.length);
}
player.x = laneCenters[newLane];
LK.getSound('teleport').play();
bonusTxt.setText("🌀 TELEPORT! (Random lane)");
LK.effects.flashObject(player, 0x4fc3f7, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'clear') {
// Clear all obstacles and enemy cars
for (ci = enemyCars.length - 1; ci >= 0; ci--) {
enemyCars[ci].destroy();
enemyCars.splice(ci, 1);
}
for (oi = obstacles.length - 1; oi >= 0; oi--) {
obstacles[oi].destroy();
obstacles.splice(oi, 1);
}
LK.getSound('clear').play();
bonusTxt.setText("🧹 CLEAR! (All obstacles gone)");
LK.effects.flashObject(player, 0x81d4fa, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'magnet') {
// Magnet: attract bonuses for 8 seconds
player._magnetTicks = 480;
LK.getSound('magnet').play();
bonusTxt.setText("🧲 MAGNET! (Bonuses come to you)");
LK.effects.flashObject(player, 0xff8a65, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type === 'reverse') {
// Reverse: enemies move up for 5 seconds
game._reverseTicks = 300;
LK.getSound('reverse').play();
bonusTxt.setText("🔄 REVERSE! (Enemies go up!)");
LK.effects.flashObject(player, 0xba68c8, 600);
(function () {
var t = bonusTxt;
t.alpha = 1;
tween(t, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else if (b.type.indexOf('score') === 0) {
// All score bonuses
addScore = 100; // Optionally, make higher-numbered bonuses worth more
idx = 0;
if (b.type.length > 5) {
idx = parseInt(b.type.substring(5), 10) - 1;
if (isNaN(idx) || idx < 0) idx = 0;
}
addScore += idx * 10; // e.g. score17 gives 100+16*10=260
if (player._doubleScore) addScore *= 2;
bonusScore += addScore;
score += addScore;
LK.getSound('bonus').play();
LK.effects.flashObject(player, 0x43a047, 400);
bonusTxt.setText("💰 +" + addScore + "! (Bonus " + b.type.replace('score', '') + ")");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
} else {
// fallback
addScore = 100;
if (player._doubleScore) addScore *= 2;
bonusScore += addScore;
score += addScore;
LK.getSound('bonus').play();
LK.effects.flashObject(player, 0x43a047, 400);
bonusTxt.setText("💰 +" + addScore + "! (Score Bonus)");
(function () {
var t = bonusTxt;
t.alpha = 1;
t.scaleX = t.scaleY = 1.4;
tween(t, {
scaleX: 1,
scaleY: 1,
alpha: 0
}, {
duration: 900,
easing: tween.easeOutBack,
onFinish: function onFinish() {
t.setText('');
}
});
})();
}
b.destroy();
bonuses.splice(i, 1);
}
},
b,
overlapped,
bBounds,
j,
e,
eBounds,
j,
o,
oBounds,
j,
h,
hBounds,
autoCollectBonus,
playerBounds,
bBoundsForgiving,
bonusCollectMargin,
px,
py,
bx,
by,
dx,
dy,
maxDistX,
maxDistY,
oldLane,
li,
newLane,
ci,
oi,
addScore,
idx,
addScore,
i,
h,
overlapped,
hBounds,
j,
other,
otherBounds,
j,
e,
eBounds,
j,
o,
oBounds,
j,
b,
bBounds,
autoCollectHearth,
playerBounds,
hBounds,
hearthCollectMargin,
px,
py,
hx,
hy,
dx,
dy,
maxDistX,
maxDistY,
oldLane,
li,
newLane,
ci,
oi,
addScore,
idx,
addScore,
_ret3;
for (var i = bonuses.length - 1; i >= 0; i--) {
_ret3 = _loop3();
if (_ret3 === 0) continue;
}
// Score increases with distance
if (!lastCrash) {
var addScore = Math.floor(gameSpeed / 8); // Skor artışını daha da yavaşlat (daha küçük bir oran)
if (player._doubleScore) addScore *= 2;
score += addScore;
scoreTxt.setText(score + "");
// Update high score if needed
if (score > highScore) {
highScore = score;
storage.highScore = highScore;
if (typeof highScoreTxt !== "undefined") {
highScoreTxt.setText('HIGH SCORE: ' + highScore);
}
}
// Animate score text on score increase
scoreTxt.scaleX = scoreTxt.scaleY = 1.25;
tween(scoreTxt, {
scaleX: 1,
scaleY: 1
}, {
duration: 220,
easing: tween.easeOutBack
});
// Handle double score timer
if (player._doubleScore) {
player._doubleScoreTicks--;
if (player._doubleScoreTicks <= 0) {
player._doubleScore = 0;
player._doubleScoreTicks = 0;
}
}
// Handle slowmo timer
if (game._slowmoTicks && game._slowmoTicks > 0) {
game._slowmoTicks--;
if (game._slowmoTicks === 0 && typeof game._origGameSpeed !== "undefined") {
gameSpeed = game._origGameSpeed;
game._origGameSpeed = undefined;
// Patlatılacak düşman ve engelleri yok et
if (game._slowmoExplodeQueue) {
// Patlat enemyCars
for (var i = 0; i < game._slowmoExplodeQueue.enemies.length; i++) {
var e = game._slowmoExplodeQueue.enemies[i];
LK.getSound('crash').play();
LK.effects.flashObject(e, 0x80cbc4, 200);
tween(e, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0
}, {
duration: 350,
easing: tween.easeOutCubic,
onFinish: function (obj) {
return function () {
obj.destroy();
};
}(e)
});
}
// Patlat obstacles
for (var i = 0; i < game._slowmoExplodeQueue.obstacles.length; i++) {
var o = game._slowmoExplodeQueue.obstacles[i];
LK.getSound('crash').play();
LK.effects.flashObject(o, 0x80cbc4, 200);
tween(o, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0
}, {
duration: 350,
easing: tween.easeOutCubic,
onFinish: function (obj) {
return function () {
obj.destroy();
};
}(o)
});
}
game._slowmoExplodeQueue = null;
}
}
}
// When slowmo is active, do NOT affect hearthBonuses or bonuses speed
if (game._slowmoTicks && game._slowmoTicks > 0) {
// Only update gameSpeed for enemyCars and obstacles, not for hearthBonuses or bonuses
for (var i = 0; i < enemyCars.length; i++) {
enemyCars[i].speed = gameSpeed;
}
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].speed = gameSpeed;
}
// Do NOT change speed for hearthBonuses or bonuses
for (var i = 0; i < bonuses.length; i++) {
// Always keep bonus asset at its own speed, never override during slowmo
// No action needed, just a reminder
}
} else {
// Restore all speeds to current gameSpeed
for (var i = 0; i < enemyCars.length; i++) {
enemyCars[i].speed = gameSpeed;
}
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].speed = gameSpeed;
}
// Do NOT change speed for hearthBonuses or bonuses
for (var i = 0; i < bonuses.length; i++) {
// Always keep bonus asset at its own speed, never override during normal
// No action needed, just a reminder
}
}
// Always keep bonuses and hearthBonuses at their own speed, never override their .speed during slowmo or normal
// Handle shrink timer
if (player._shrinkTicks && player._shrinkTicks > 0) {
player._shrinkTicks--;
if (player._shrinkTicks === 0) {
player.scaleX = player.scaleY = 0.55;
// Restore width/height to original (not cumulative shrink)
player.width = player._carAsset.width * 0.55;
player.height = player._carAsset.height * 0.55;
}
}
// Handle invincible timer
if (player._invincible && player._invincibleTicks > 0) {
player._invincibleTicks--;
if (player._invincibleTicks === 0) {
player._invincible = 0;
}
}
// Handle magnet timer
if (player._magnetTicks && player._magnetTicks > 0) {
player._magnetTicks--;
// Attract bonuses
for (var mi = 0; mi < bonuses.length; mi++) {
var bonusObj = bonuses[mi];
var dx = player.x - bonusObj.x;
var dy = player.y - bonusObj.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 600) {
// Move bonus towards player
bonusObj.x += dx * 0.08;
bonusObj.y += dy * 0.08;
}
}
// Attract hearth bonuses
for (var hi = 0; hi < hearthBonuses.length; hi++) {
var hearthObj = hearthBonuses[hi];
var dxh = player.x - hearthObj.x;
var dyh = player.y - hearthObj.y;
var disth = Math.sqrt(dxh * dxh + dyh * dyh);
if (disth < 600) {
// Move hearth towards player
hearthObj.x += dxh * 0.08;
hearthObj.y += dyh * 0.08;
}
}
if (player._magnetTicks === 0) {
// End magnet
}
}
// Handle reverse timer
if (game._reverseTicks && game._reverseTicks > 0) {
game._reverseTicks--;
// Reverse enemy and obstacle movement
if (!game._reverseExplodeQueue) game._reverseExplodeQueue = {
enemies: [],
obstacles: []
};
for (var ri = enemyCars.length - 1; ri >= 0; ri--) {
var e = enemyCars[ri];
e.y -= gameSpeed * 2;
// If enemy car goes off the top, queue for explosion after reverse ends
if (e.y + e.height / 2 < -100) {
game._reverseExplodeQueue.enemies.push(e);
enemyCars.splice(ri, 1);
}
}
for (var ro = obstacles.length - 1; ro >= 0; ro--) {
var o = obstacles[ro];
o.y -= gameSpeed * 2;
// If obstacle goes off the top, queue for explosion after reverse ends
if (o.y + o.height / 2 < -100) {
game._reverseExplodeQueue.obstacles.push(o);
obstacles.splice(ro, 1);
}
}
// Do NOT affect hearthBonuses or bonuses during reverse
if (game._reverseTicks === 0) {
// End reverse: now explode all queued enemies and obstacles
if (game._reverseExplodeQueue) {
// Explode enemy cars
for (var i = 0; i < game._reverseExplodeQueue.enemies.length; i++) {
var e = game._reverseExplodeQueue.enemies[i];
LK.getSound('crash').play();
// Explosion effect: scale up, fade out, flash red
LK.effects.flashObject(e, 0xff0000, 200);
tween(e, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0
}, {
duration: 350,
easing: tween.easeOutCubic,
onFinish: function (obj) {
return function () {
obj.destroy();
};
}(e)
});
}
// Explode obstacles
for (var i = 0; i < game._reverseExplodeQueue.obstacles.length; i++) {
var o = game._reverseExplodeQueue.obstacles[i];
LK.getSound('crash').play();
LK.effects.flashObject(o, 0xff0000, 200);
tween(o, {
scaleX: 1.7,
scaleY: 1.7,
alpha: 0
}, {
duration: 350,
easing: tween.easeOutCubic,
onFinish: function (obj) {
return function () {
obj.destroy();
};
}(o)
});
}
game._reverseExplodeQueue = null;
}
}
}
// Do NOT affect hearthBonuses or bonuses during reverse
// --- Update bonus icon position and state ---
bonusIcon.x = player.x;
bonusIcon.y = player.y - player.height / 2 - 30;
// Determine which bonus is active and show icon
var showIcon = '';
if (player._invincible && player._invincibleTicks > 0) {
showIcon = '🦾';
bonusIcon.setText('🦾');
bonusIcon.alpha = 1;
} else if (player._shield) {
showIcon = '🛡️';
bonusIcon.setText('🛡️');
bonusIcon.alpha = 1;
} else if (player._doubleScore && player._doubleScoreTicks > 0) {
showIcon = '✨';
bonusIcon.setText('✨');
bonusIcon.alpha = 1;
} else if (player._shrinkTicks && player._shrinkTicks > 0) {
showIcon = '🔽';
bonusIcon.setText('🔽');
bonusIcon.alpha = 1;
} else if (player._magnetTicks && player._magnetTicks > 0) {
showIcon = '🧲';
bonusIcon.setText('🧲');
bonusIcon.alpha = 1;
} else if (game._slowmoTicks && game._slowmoTicks > 0) {
showIcon = '🐢';
bonusIcon.setText('🐢');
bonusIcon.alpha = 1;
} else if (game._reverseTicks && game._reverseTicks > 0) {
showIcon = '🔄';
bonusIcon.setText('🔄');
bonusIcon.alpha = 1;
} else {
bonusIcon.setText('');
bonusIcon.alpha = 0;
}
// Play win sound if score threshold reached (example: 10000)
if (score >= 10000 && !game._winSoundPlayed) {
LK.getSound('win').play();
game._winSoundPlayed = true;
}
}
};
// Reset crash state on new game
LK.on('gameStart', function () {
lastCrash = false;
playerHealth = MAX_PLAYER_HEALTH;
updateHealthBar();
score = 0;
bonusScore = 0;
bonusUsageCount = 0;
scoreTxt.setText('0');
bonusTxt.setText('');
if (typeof highScoreTxt !== "undefined") {
highScore = storage.highScore || 0;
highScoreTxt.setText('HIGH SCORE: ' + highScore);
}
if (typeof bonusUsageTxt !== "undefined") {
bonusUsageTxt.setText("BONUS: 0");
}
// Remove all cars, obstacles, bonuses
for (var i = enemyCars.length - 1; i >= 0; i--) {
enemyCars[i].destroy();
enemyCars.splice(i, 1);
}
for (var i = obstacles.length - 1; i >= 0; i--) {
obstacles[i].destroy();
obstacles.splice(i, 1);
}
for (var i = bonuses.length - 1; i >= 0; i--) {
bonuses[i].destroy();
bonuses.splice(i, 1);
}
for (var i = hearthBonuses.length - 1; i >= 0; i--) {
hearthBonuses[i].destroy();
hearthBonuses.splice(i, 1);
}
playerTargetLane = 1;
player.x = laneCenters[1];
player.y = 2732 - 500;
if (playerMoveTween && playerMoveTween.cancel) playerMoveTween.cancel();
player._shield = 0;
player._doubleScore = 0;
player._doubleScoreTicks = 0;
player._shrinkTicks = 0;
player._invincible = 0;
player._invincibleTicks = 0;
player._magnetTicks = 0;
player.scaleX = player.scaleY = 0.55;
player.width = player._carAsset.width * 0.55;
player.height = player._carAsset.height * 0.55;
game._slowmoTicks = 0;
game._reverseTicks = 0;
gameSpeed = 18;
ticksSinceStart = 0;
lastSpawnTick = 0;
lastBonusTick = 0;
game._spawnInterval = 36; // Easy stage spawn interval
// Reset bonus icon
if (typeof bonusIcon !== "undefined") {
bonusIcon.setText('');
bonusIcon.alpha = 0;
}
});
// Prevent player from moving into top left menu area
// (handled by roadLeft, but double check)
if (roadLeft < 100) {
roadLeft = 100;
roadWidth = roadRight - roadLeft;
laneWidth = roadWidth / laneCount;
for (var i = 0; i < laneCount; i++) {
laneCenters[i] = roadLeft + laneWidth / 2 + i * laneWidth;
}
}
// Play background music and engine idle at game start
LK.on('gameStart', function () {
LK.playMusic('bgmusic');
LK.getSound('engine_idle').play();
});
bonus. In-Game asset. 2d. High contrast. No shadows
obstackle. In-Game asset. 2d. High contrast. No shadows
Rear and top view of cool luxury sports car looking upwards. In-Game asset. 2d. High contrast. No shadows
top view of plant looking upwards. In-Game asset. 2d. High contrast. No shadows
Top view of colorful yellow plant looking upwards. In-Game asset. 2d. High contrast. No shadows
Top view of colorful pink plant looking upwards. In-Game asset. 2d. High contrast. No shadows
Top view of colorful pink tree looking upwards. In-Game asset. 2d. High contrast. No shadows
Top view of colorful tree looking upwards. In-Game asset. 2d. High contrast. No shadows
hearth red. In-Game asset. 2d. High contrast. No shadows
cool luxury sports car bumper view looking up. In-Game asset. 2d. High contrast. No shadows