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