/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Enemy Car Class // Note: The car sprite is always the first child (self.children[0]) for tinting purposes. var EnemyCar = Container.expand(function () { var self = Container.call(this); var car = self.attachAsset('enemyCar', { anchorX: 0.5, anchorY: 0.5 }); // --- Overlay a white rectangle for the window --- // The window is always in the same place on the enemyCar asset, so we overlay a white box. // These values are tuned for the asset's window position and size. var windowOverlay = LK.getAsset('heartIcon', { anchorX: 0.5, anchorY: 0.5, width: 70, height: 38, x: 0, y: -38, tint: 0xffffff, alpha: 1 }); self.addChild(windowOverlay); self.lane = 1; self.speed = 18; self.update = function () { self.y += self.speed * gameSpeed; }; return self; }); // Fireball Class var Fireball = Container.expand(function () { var self = Container.call(this); var fireball = self.attachAsset('Fireball', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 28; self.lane = 1; // --- Health bar for Fireball (ace) --- self.maxLife = 3; self.life = self.maxLife; // Health bar background self.lifeBarBg = self.attachAsset('lifeBarBg', { anchorX: 0.5, anchorY: 0.5, width: 220, height: 220, y: -90, alpha: 0.92 }); // Health bar border self.lifeBarBorder = self.attachAsset('lifeBarBorder', { anchorX: 0.5, anchorY: 0.5, width: 260, height: 260, y: -90, alpha: 1 }); // Health bar fill self.lifeBar = self.attachAsset('lifeBar', { anchorX: 0.0, anchorY: 0.5, width: 180, height: 60, x: -90, y: -90, alpha: 1 }); // Heart icon self.lifeBarHeart = self.attachAsset('heartIcon', { anchorX: 0.5, anchorY: 0.5, width: 70, height: 70, x: -130, y: -90, alpha: 1 }); // Helper to update fireball health bar UI self.updateLifeBar = function () { // Show only if not full and not dead var show = self.life < self.maxLife && self.life > 0; self.lifeBarBg.visible = show; self.lifeBar.visible = show; self.lifeBarBorder.visible = show; self.lifeBarHeart.visible = show; // Set width proportional to life var minWidth = 24; var w = self.life > 0 ? Math.max(minWidth, Math.round(180 * (self.life / self.maxLife))) : 0; self.lifeBar.width = w; // Hide if dead if (self.life <= 0) { self.lifeBarBg.visible = false; self.lifeBar.visible = false; self.lifeBarBorder.visible = false; self.lifeBarHeart.visible = false; } }; // Initialize bar state self.updateLifeBar(); self.update = function () { // Move fireball down the screen self.y += self.speed * gameSpeed; // Keep health bar above fireball self.lifeBarBg.y = -90; self.lifeBarBorder.y = -90; self.lifeBar.y = -90; self.lifeBarHeart.y = -90; self.lifeBar.x = -90; self.lifeBarHeart.x = -130; self.updateLifeBar(); }; return self; }); // Lane Divider Class var LaneDivider = Container.expand(function () { var self = Container.call(this); var div = self.attachAsset('laneDivider', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 18; self.update = function () { self.y += self.speed * gameSpeed; }; 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.lane = 1; self.speed = 18; self.update = function () { self.y += self.speed * gameSpeed; }; 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 }); self.lane = 1; // 0: left, 1: center, 2: right self.life = playerMaxLife; // Add life property to player car self.setLane = function (laneIdx) { self.lane = laneIdx; // Animate to new lane position var targetX = lanes[self.lane]; // Always reset Y to baseY for lane change if (typeof self.baseY === "undefined") self.baseY = 2732 - 500; tween.stop(self, { scaleX: true, scaleY: true, y: true }); // Squash/stretch effect: squash horizontally, stretch vertically, then restore tween(self, { scaleX: 1.18, scaleY: 0.88 }, { duration: 60, easing: tween.cubicOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 80, easing: tween.cubicOut }); } }); // Move to new lane and reset Y to baseY (so bounce resumes from correct position) tween(self, { x: targetX, y: self.baseY }, { duration: 120, easing: tween.cubicOut, onUpdate: function onUpdate() { updateLifeBar(); } }); updateLifeBar(); }; // Idle bounce animation for player car self.baseY = 2732 - 500; // Store the base Y for idle bounce self.idleBounce = function () { // Cancel any previous bounce tween.stop(self, { y: true }); // Animate up tween(self, { y: self.baseY - 18 }, { duration: 320, easing: tween.sineInOut, onFinish: function onFinish() { // Animate down tween(self, { y: self.baseY + 18 }, { duration: 320, easing: tween.sineInOut, onFinish: function onFinish() { // Loop bounce self.idleBounce(); } }); } }); }; // Start idle bounce when created self.idleBounce(); // Clamp Y position to prevent slipping up self.update = function () { // If y is above the allowed minimum, clamp it back var minY = 2732 - 700; // Don't let car go above this Y (adjust as needed) if (self.y < minY) { self.y = minY; } }; return self; }); // --- PEOPLE WALKING ON SIDEWALKS --- // Person class for sidewalk walkers var SidewalkPerson = Container.expand(function () { var self = Container.call(this); // Use heartIcon as a placeholder for a person (circle/box) var person = self.attachAsset('heartIcon', { anchorX: 0.5, anchorY: 0.5, width: 60, height: 60, tint: 0x333333 + Math.floor(Math.random() * 0xCCCCCC), // random grayish color alpha: 1 }); // Add hands (arms) as rectangles var leftHand = LK.getAsset('heartIcon', { anchorX: 0.5, anchorY: 0.5, width: 16, height: 38, x: -32, y: 18, tint: 0xdeb887, // light brown, can randomize for variety alpha: 1 }); var rightHand = LK.getAsset('heartIcon', { anchorX: 0.5, anchorY: 0.5, width: 16, height: 38, x: 32, y: 18, tint: 0xdeb887, alpha: 1 }); self.addChild(leftHand); self.addChild(rightHand); // Randomize direction: 1 = down, -1 = up self.dir = Math.random() < 0.5 ? 1 : -1; // Set speed (pixels per frame) self.speed = 4 + Math.random() * 2; // Set sidewalk: 0 = left, 1 = right self.side = 0; // Set initial position if (Math.random() < 0.5) { self.side = 0; self.x = sidewalkWidth / 2; } else { self.side = 1; self.x = 2048 - sidewalkWidth / 2; } // Y: if going down, start at top; if up, start at bottom self.y = self.dir === 1 ? -40 - Math.random() * 200 : 2732 + 40 + Math.random() * 200; // Sway animation self.swayTick = Math.random() * Math.PI * 2; self.update = function () { // Move up or down self.y += self.speed * self.dir; // Sway left/right a little self.swayTick += 0.08; var sway = Math.sin(self.swayTick) * 10; self.x = (self.side === 0 ? sidewalkWidth / 2 : 2048 - sidewalkWidth / 2) + sway; // --- Magic bottle in hand logic --- if (typeof self.handBottle === "undefined") { self.handBottle = null; } if (typeof self.craveSpellBottle === "undefined") self.craveSpellBottle = Math.random() < 0.5; // 50% crave // If craving and not on cooldown, show bottle in hand if (self.craveSpellBottle && self.throwCooldown <= 0 && !self.handBottle) { // Add a bottle in the hand (side dependent) self.handBottle = LK.getAsset('spellBottle', { anchorX: 0.5, anchorY: 0.5, width: 38, height: 38, x: self.side === 0 ? -32 : 32, // left or right hand y: 18, tint: 0x2BD5E6, alpha: 1 }); self.addChild(self.handBottle); } // If not craving or on cooldown, remove bottle from hand if present if ((!self.craveSpellBottle || self.throwCooldown > 0) && self.handBottle) { self.handBottle.destroy(); self.handBottle = null; } }; return self; }); /**** * Initialize Game ****/ // Create and track sidewalk people var game = new LK.Game({ backgroundColor: 0x222222 }); /**** * Game Code ****/ // Use spellBottle as heart // Health bar assets // Spell bottle asset (example: blue bottle, 120x180) // When the player picks up the magic bottle, the car bursts into flames (flame burst effect) // Lane positions (3 lanes) // Car (player) // Enemy car // Obstacle (barrier) // Road lane divider // Sound for crash // Sound for lane change // Music (background) // Person drawing asset for sidewalk walkers (customize this for identity) // This is a simple stick-figure style: head (ellipse), body (box), arms (boxes), legs (boxes) var laneCount = 3; var laneWidth = 520; // Increased lane width for more space between lanes var lanes = [2048 / 2 - laneWidth, // left 2048 / 2, // center 2048 / 2 + laneWidth // right ]; // Game variables var playerCar; var enemyCars = []; var obstacles = []; var laneDividers = []; var score = 0; var highScore = storage.highScore || 0; var scoreTxt; var highScoreTxt; var gameSpeed = 1; var ticksSinceStart = 0; var swipeStartX = null; var swipeStartY = null; var swipeActive = false; var lastLane = 1; var spawnTick = 0; var dividerSpacing = 320; var dragNode = null; // --- LANE IDLE TIMER SYSTEM --- var laneIdleTicks = [0, 0, 0]; // Track how long player stays in each lane (in ticks) var lastPlayerLane = 1; // Track last lane for idle timer var fireballIdleTriggered = [false, false, false]; // Prevent multiple fireballs for same idle period // --- LIFE SYSTEM --- var playerMaxLife = 5; var playerLife = playerMaxLife; // Health bar UI will be handled by a new, clear, and consistent system below // Score display scoreTxt = new Text2('0', { size: 120, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0.5); LK.gui.top.addChild(scoreTxt); // Shield timer display var shieldTxt = new Text2('', { size: 80, fill: 0x2BD5E6 }); shieldTxt.anchor.set(0.5, 0.5); LK.gui.top.addChild(shieldTxt); // High Score display highScoreTxt = new Text2('HI: ' + highScore, { size: 60, fill: 0xFFFF00 }); highScoreTxt.anchor.set(0.5, 0.5); LK.gui.top.addChild(highScoreTxt); // Add swipe left gesture to high score text var hiSwipeStartX = null; var hiSwipeStartY = null; var hiSwipeActive = false; highScoreTxt.down = function (x, y, obj) { hiSwipeStartX = x; hiSwipeStartY = y; hiSwipeActive = true; }; highScoreTxt.move = function (x, y, obj) { if (!hiSwipeActive) return; var dx = x - hiSwipeStartX; var dy = y - hiSwipeStartY; // Only consider horizontal swipes, ignore vertical if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy)) { if (dx < 0) { // Swipe left detected on high score text // Flash the high score text white for feedback tween(highScoreTxt, { tint: 0xffffff }, { duration: 80, yoyo: true, repeat: 1, onComplete: function onComplete() { highScoreTxt.tint = 0xFFFF00; } }); hiSwipeActive = false; } } }; highScoreTxt.up = function (x, y, obj) { hiSwipeActive = false; }; // Start music LK.playMusic('bgmusic'); // Create player car playerCar = new PlayerCar(); playerCar.x = lanes[1]; playerCar.y = 2732 - 500; playerCar.setLane(1); // Shield properties playerCar.shieldActive = false; playerCar.shieldTicks = 0; // Add shield effect asset (invisible by default) var shieldEffect = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.2, scaleY: 1.2, x: playerCar.x, y: playerCar.y, alpha: 0.5, tint: 0xffffff }); shieldEffect.visible = false; game.addChild(shieldEffect); game.addChild(playerCar); // Add sidewalks to the far left and right edges of the screen (do not touch the road) var sidewalkWidth = 120; var sidewalkHeight = 2732; var sidewalkColor = 0xcccccc; // Light gray for sidewalk var leftSidewalk = LK.getAsset('heartIcon', { anchorX: 0, anchorY: 0, width: sidewalkWidth, height: sidewalkHeight, x: 0, y: 0, tint: sidewalkColor, alpha: 1 }); var rightSidewalk = LK.getAsset('heartIcon', { anchorX: 0, anchorY: 0, width: sidewalkWidth, height: sidewalkHeight, x: 2048 - sidewalkWidth, y: 0, tint: sidewalkColor, alpha: 1 }); game.addChild(leftSidewalk); game.addChild(rightSidewalk); // Create and track sidewalk people var sidewalkPeople = []; var maxPeoplePerSide = 6; for (var i = 0; i < maxPeoplePerSide * 2; i++) { var p = new SidewalkPerson(); sidewalkPeople.push(p); game.addChild(p); } // Create lane dividers (vertical lines for each lane) // For 3 lanes, we want to copy the center lane divider and place it to the right and left of the center stripe, without adding extra lines // The center divider is between lane 0 and lane 1, and between lane 1 and lane 2 // So, for 3 lanes, we want to draw the two dividers: one between lane 0 and 1, and one between lane 1 and 2 var dividerOffsets = []; // Find the X positions for the two dividers (between lanes) for (var l = 1; l < laneCount; l++) { dividerOffsets.push((lanes[l - 1] + lanes[l]) / 2); } // Now, for each divider, draw a set of stripes down the screen for (var d = 0; d < dividerOffsets.length; d++) { for (var i = 0; i < 10; i++) { // Center LaneDivider var divider = new LaneDivider(); divider.x = dividerOffsets[d]; divider.y = i * dividerSpacing; laneDividers.push(divider); game.addChild(divider); // Left copy var dividerLeft = new LaneDivider(); dividerLeft.x = dividerOffsets[d] - laneWidth; dividerLeft.y = i * dividerSpacing; laneDividers.push(dividerLeft); game.addChild(dividerLeft); // Right copy var dividerRight = new LaneDivider(); dividerRight.x = dividerOffsets[d] + laneWidth; dividerRight.y = i * dividerSpacing; laneDividers.push(dividerRight); game.addChild(dividerRight); } } // Prepare for dynamic spell bottle spawning var spellBottle = null; var spellBottleActive = false; var spellBottleLane = 1; // Add: track last spell bottle spawn tick to limit spawn rate var lastSpellBottleSpawnTick = -1000; // Helper: spawn enemy car, obstacle, or fireball function spawnObstacleOrEnemy() { var y = -300; if (score >= 400) { // After 400 points, restore original spawn logic but with higher speed (handled by gameSpeed) // Randomly decide: 60% enemy car, 25% obstacle, 15% fireball var laneIdx = Math.floor(Math.random() * laneCount); var rand = Math.random(); if (rand < 0.6) { var enemy = new EnemyCar(); enemy.lane = laneIdx; enemy.x = lanes[laneIdx]; enemy.y = y; // Assign a bold, high-contrast color tint to the enemy car for visibility var enemyTints = [0xFFFFFF, 0xFF3333, 0x33CCFF, 0xFFCC00, 0x00FF66, 0xFF66FF, 0xFF8800, 0x9933FF]; var randomColor = enemyTints[Math.floor(Math.random() * enemyTints.length)]; if (enemy.children && enemy.children.length > 0) { enemy.children[0].tint = randomColor; } enemyCars.push(enemy); game.addChild(enemy); } else if (rand < 0.85) { var obs = new Obstacle(); obs.lane = laneIdx; obs.x = lanes[laneIdx]; obs.y = y; obstacles.push(obs); game.addChild(obs); } else { // Prevent fireballs from spawning before 60 points if (score >= 60) { // Limit fireballs: only spawn if fewer than 2 are on screen if (!window.fireballs) window.fireballs = []; if (window.fireballs.length < 2) { var fireball = new Fireball(); fireball.lane = laneIdx; fireball.x = lanes[laneIdx]; fireball.y = y; window.fireballs.push(fireball); game.addChild(fireball); } } } } else if (score > 200) { // After 200 points, only spawn barriers (obstacles), and spawn in 2 lanes at once // Pick two different lanes var lanesToUse = []; while (lanesToUse.length < 2) { var idx = Math.floor(Math.random() * laneCount); if (lanesToUse.indexOf(idx) === -1) lanesToUse.push(idx); } for (var i = 0; i < lanesToUse.length; i++) { var obs = new Obstacle(); obs.lane = lanesToUse[i]; obs.x = lanes[lanesToUse[i]]; obs.y = y; obstacles.push(obs); game.addChild(obs); } } else if (score >= 70) { // Only spawn fireballs after 70 points if (!window.fireballs) window.fireballs = []; if (window.fireballs.length < 2) { var laneIdx = Math.floor(Math.random() * laneCount); var fireball = new Fireball(); fireball.lane = laneIdx; fireball.x = lanes[laneIdx]; fireball.y = y; window.fireballs.push(fireball); game.addChild(fireball); } } else { // Randomly decide: 60% enemy car, 25% obstacle, 15% fireball var laneIdx = Math.floor(Math.random() * laneCount); var rand = Math.random(); if (rand < 0.6) { var enemy = new EnemyCar(); enemy.lane = laneIdx; enemy.x = lanes[laneIdx]; enemy.y = y; // Assign a bold, high-contrast color tint to the enemy car for visibility // Use a set of bold, high-contrast tints for random color var enemyTints = [0xFFFFFF, // white 0xFF3333, // red 0x33CCFF, // blue 0xFFCC00, // yellow 0x00FF66, // green 0xFF66FF, // magenta 0xFF8800, // orange 0x9933FF // purple ]; var randomColor = enemyTints[Math.floor(Math.random() * enemyTints.length)]; if (enemy.children && enemy.children.length > 0) { enemy.children[0].tint = randomColor; } enemyCars.push(enemy); game.addChild(enemy); } else if (rand < 0.85) { var obs = new Obstacle(); obs.lane = laneIdx; obs.x = lanes[laneIdx]; obs.y = y; obstacles.push(obs); game.addChild(obs); } else { // Prevent fireballs from spawning before 60 points if (score >= 60) { // Limit fireballs: only spawn if fewer than 2 are on screen if (!window.fireballs) window.fireballs = []; if (window.fireballs.length < 2) { var fireball = new Fireball(); fireball.lane = laneIdx; fireball.x = lanes[laneIdx]; fireball.y = y; window.fireballs.push(fireball); game.addChild(fireball); } } } } } // Helper: update score function updateScore(val) { score = val; scoreTxt.setText('' + score); // Instantly update score text if (score > highScore) { highScore = score; highScoreTxt.setText('HI: ' + highScore); storage.highScore = highScore; } } // Helper: update player health bar UI (global, for use in PlayerCar.setLane and elsewhere) function updateLifeBar() { // Always show the health bar unless dead var showBar = playerLife > 0; if (window.playerHealthBarBg) window.playerHealthBarBg.visible = showBar; if (window.playerHealthBarFill) window.playerHealthBarFill.visible = showBar; if (window.playerHealthBarBorder) window.playerHealthBarBorder.visible = showBar; // Removed: if (window.playerHealthBarHeart) window.playerHealthBarHeart.visible = showBar; // Position above player car if (playerCar && window.playerHealthBarBg && window.playerHealthBarFill && window.playerHealthBarBorder) { var barY = playerCar.y - 140; var barX = playerCar.x; window.playerHealthBarBg.x = barX; window.playerHealthBarBg.y = barY; window.playerHealthBarFill.x = barX - 240; window.playerHealthBarFill.y = barY; window.playerHealthBarBorder.x = barX; window.playerHealthBarBorder.y = barY; // Removed: window.playerHealthBarHeart.x = barX - 320; // Removed: window.playerHealthBarHeart.y = barY; // Set width proportional to life, always show a minimum width if not dead var minWidth = 38; var fillW = playerLife > 0 ? Math.max(minWidth, Math.round(480 * (playerLife / playerMaxLife))) : 0; window.playerHealthBarFill.width = fillW; } // Hide if dead if (playerLife <= 0) { if (window.playerHealthBarBg) window.playerHealthBarBg.visible = false; if (window.playerHealthBarFill) window.playerHealthBarFill.visible = false; if (window.playerHealthBarBorder) window.playerHealthBarBorder.visible = false; // Removed: if (window.playerHealthBarHeart) window.playerHealthBarHeart.visible = false; } } // Touch/drag/swipe handling game.down = function (x, y, obj) { swipeStartX = x; swipeStartY = y; swipeActive = true; dragNode = playerCar; }; game.move = function (x, y, obj) { // Only handle swipe if active if (!swipeActive) return; if (!dragNode) return; var dx = x - swipeStartX; var dy = y - swipeStartY; // Only consider horizontal swipes, ignore vertical if (Math.abs(dx) > 80 && Math.abs(dx) > Math.abs(dy)) { var newLane = playerCar.lane; if (dx < 0 && playerCar.lane > 0) { newLane = playerCar.lane - 1; } else if (dx > 0 && playerCar.lane < laneCount - 1) { newLane = playerCar.lane + 1; } if (newLane !== playerCar.lane) { // Reset idle timer for previous lane, and for new lane laneIdleTicks[playerCar.lane] = 0; fireballIdleTriggered[playerCar.lane] = false; playerCar.setLane(newLane); // Also reset for new lane laneIdleTicks[newLane] = 0; fireballIdleTriggered[newLane] = false; lastPlayerLane = newLane; LK.getSound('swipe').play(); swipeActive = false; dragNode = null; } } }; game.up = function (x, y, obj) { swipeActive = false; dragNode = null; }; // Main game update loop game.update = function () { ticksSinceStart++; // --- LANE IDLE TIMER UPDATE --- // Only increment for the current lane, reset others for (var l = 0; l < laneCount; l++) { if (l === playerCar.lane) { laneIdleTicks[l]++; } else { laneIdleTicks[l] = 0; fireballIdleTriggered[l] = false; } } // If player has stayed in the same lane for >10s (600 ticks), spawn a fireball in that lane if (laneIdleTicks[playerCar.lane] > 600 && !fireballIdleTriggered[playerCar.lane]) { // Only spawn if fewer than 2 fireballs are on screen if (!window.fireballs) window.fireballs = []; if (window.fireballs.length < 2) { var fireball = new Fireball(); fireball.lane = playerCar.lane; fireball.x = lanes[playerCar.lane]; fireball.y = -300; window.fireballs.push(fireball); game.addChild(fireball); } fireballIdleTriggered[playerCar.lane] = true; // Optionally, flash a warning or effect here if desired } // Adjust acceleration: start at 20 points, increase slowly until 70 points, then ramp up if (score >= 400) { // After 400 points, restore original spawn logic but make everything much faster gameSpeed = 2.5; // Significantly faster than normal } else if (score >= 250) { // Between 250 and 400, slow down the speed a little (decelerate) if (ticksSinceStart % 60 === 0 && gameSpeed > 1.2) { gameSpeed -= 0.04; if (gameSpeed < 1.2) gameSpeed = 1.2; } } else if (score >= 70) { // After 70, only fireballs spawn and game returns to normal speed gameSpeed = 1; } else if (score >= 20) { // Between 20 and 70, increase very slowly if (ticksSinceStart % 90 === 0 && gameSpeed < 2) { gameSpeed += 0.04; if (gameSpeed > 2) gameSpeed = 2; } } else { gameSpeed = 1; } // --- UPDATE SIDEWALK PEOPLE --- for (var i = 0; i < sidewalkPeople.length; i++) { var p = sidewalkPeople[i]; p.update(); // --- GIVE ACETITY: random chance to get a speed boost and color flash --- if (typeof p.acetityCooldown === "undefined") p.acetityCooldown = 0; if (typeof p.acetityActive === "undefined") p.acetityActive = false; if (typeof p.baseSpeed === "undefined") p.baseSpeed = p.speed; // --- THROWING ITEMS: only crave (desire) spell bottles, and rarely throw them --- if (typeof p.throwCooldown === "undefined") p.throwCooldown = 0; if (typeof p.thrownItems === "undefined") p.thrownItems = []; if (typeof p.craveSpellBottle === "undefined") p.craveSpellBottle = Math.random() < 0.5; // 50% crave if (p.throwCooldown > 0) { p.throwCooldown--; } else if (p.craveSpellBottle && (score < 70 && Math.random() < 0.003 || // 0.3% chance per frame before 70 points score >= 70 && score < 400 && Math.random() < 0.001 || // 0.1% chance per frame after 70 points score >= 400 && Math.random() < 0.025 // 2.5% chance per frame after 400 points (much more frequent) ) && score >= 30) { // Only throw if not too close to top/bottom and not too many items already if (p.y > 200 && p.y < 2732 - 200 && (!window.thrownItems || window.thrownItems.length < 2)) { // Only throw spellBottle var itemType = 'spellBottle'; var thrownItem = LK.getAsset(itemType, { anchorX: 0.5, anchorY: 0.5, width: 80, height: 80, x: p.x + (p.side === 0 ? 60 : -60), // throw toward road y: p.y + 10, tint: 0x2BD5E6, alpha: 1 }); thrownItem._vx = (p.side === 0 ? 1 : -1) * (12 + Math.random() * 6); // horizontal speed toward road thrownItem._vy = 10 + Math.random() * 6; // vertical speed thrownItem._gravity = 0.7 + Math.random() * 0.2; thrownItem._rotSpeed = (Math.random() - 0.5) * 0.2; thrownItem._fromSide = p.side; thrownItem._active = true; if (!window.thrownItems) window.thrownItems = []; window.thrownItems.push(thrownItem); game.addChild(thrownItem); // Remove bottle from hand immediately after throw if (typeof p.handBottle !== "undefined" && p.handBottle) { p.handBottle.destroy(); p.handBottle = null; } p.throwCooldown = 360 + Math.floor(Math.random() * 180); // 6-9s cooldown, much less frequent } } // Cooldown for next acetity if (p.acetityCooldown > 0) { p.acetityCooldown--; } else if (!p.acetityActive && Math.random() < 0.008) { // 0.8% chance per frame to get acetity if not active p.acetityActive = true; p.acetityTicks = 60 + Math.floor(Math.random() * 60); // 1-2 seconds p.baseSpeed = p.speed; p.speed = p.baseSpeed * (1.7 + Math.random() * 0.7); // 1.7x-2.4x speed // Flash color: yellow or cyan if (p.children && p.children.length > 0) { var flashColor = Math.random() < 0.5 ? 0xffff00 : 0x00ffff; tween(p.children[0], { tint: flashColor }, { duration: 80, yoyo: true, repeat: 2, onComplete: function onComplete() { if (p.children && p.children.length > 0) { p.children[0].tint = 0x333333 + Math.floor(Math.random() * 0xCCCCCC); } } }); } } // If acetity is active, count down and restore speed after if (p.acetityActive) { p.acetityTicks--; // Optionally, make them slightly larger while acetity is active if (p.children && p.children.length > 0) { p.children[0].scaleX = 1.18; p.children[0].scaleY = 1.18; } if (p.acetityTicks <= 0) { p.acetityActive = false; p.speed = p.baseSpeed; p.acetityCooldown = 120 + Math.floor(Math.random() * 120); // 2-4s cooldown if (p.children && p.children.length > 0) { p.children[0].scaleX = 1; p.children[0].scaleY = 1; } } } // If off screen, respawn at the other end with new speed/direction if (p.dir === 1 && p.y > 2732 + 80 || p.dir === -1 && p.y < -80) { // Randomize direction p.dir = Math.random() < 0.5 ? 1 : -1; // Randomize sidewalk p.side = Math.random() < 0.5 ? 0 : 1; p.x = p.side === 0 ? sidewalkWidth / 2 : 2048 - sidewalkWidth / 2; // Y: if going down, start at top; if up, start at bottom p.y = p.dir === 1 ? -40 - Math.random() * 200 : 2732 + 40 + Math.random() * 200; // Randomize speed p.speed = 4 + Math.random() * 2; p.baseSpeed = p.speed; p.acetityActive = false; p.acetityCooldown = 0; // Randomize color if (p.children && p.children.length > 0) { p.children[0].tint = 0x333333 + Math.floor(Math.random() * 0xCCCCCC); p.children[0].scaleX = 1; p.children[0].scaleY = 1; } // Randomize sway phase p.swayTick = Math.random() * Math.PI * 2; } } // Move lane dividers, loop to top for (var i = 0; i < laneDividers.length; i++) { var div = laneDividers[i]; div.update(); if (div.y > 2732 + 60) { div.y -= dividerSpacing * 10; } } // --- UPDATE THROWN ITEMS (from sidewalk people) --- if (!window.thrownItems) window.thrownItems = []; for (var ti = window.thrownItems.length - 1; ti >= 0; ti--) { var item = window.thrownItems[ti]; if (!item._active) continue; // Move item item.x += item._vx; item.y += item._vy; item._vy += item._gravity; item.rotation += item._rotSpeed; // If item is off screen, destroy if (item.x < -100 || item.x > 2048 + 100 || item.y > 2732 + 100) { item.destroy(); window.thrownItems.splice(ti, 1); continue; } // If item reaches the road area (not sidewalk), allow collision with player var roadLeft = sidewalkWidth + 40; var roadRight = 2048 - sidewalkWidth - 40; if (item.x > roadLeft && item.x < roadRight && item.y > 0 && item.y < 2732) { // Check collision with playerCar if (playerCar && item.intersects(playerCar)) { // If it's a spellBottle (all thrown items are), grant shield to player AND increase life bar if (!playerCar.shieldActive) { playerCar.shieldActive = true; playerCar.shieldTicks = 180; // 3 seconds at 60fps // Add flame burst effect: flashObject with orange/yellow color, then white var fireMagic = LK.getAsset('spellBottle', { anchorX: 0.5, anchorY: 0.5, x: playerCar.x, y: playerCar.y, scaleX: 1, scaleY: 1, alpha: 0.85, tint: 0xff6600 }); game.addChild(fireMagic); tween(fireMagic, { scaleX: 2.2, scaleY: 2.2, alpha: 0 }, { duration: 1000, easing: tween.cubicOut, onUpdate: function onUpdate() { fireMagic.x = playerCar.x; fireMagic.y = playerCar.y; }, onComplete: function onComplete() { fireMagic.destroy(); } }); LK.effects.flashObject(playerCar, 0xffa500, 350); LK.setTimeout(function () { LK.effects.flashObject(playerCar, 0xffff00, 350); }, 350); } // Always increase life bar by 1 (up to max) when collecting a thrown bottle if (playerLife < playerMaxLife) { playerLife++; playerCar.life = playerLife; updateLifeBar(); } // Remove item after hit item.destroy(); window.thrownItems.splice(ti, 1); continue; } } } // Spell bottle no longer spawns on the road spellBottle = null; spellBottleActive = false; // Spawn enemies/obstacles every 40-60 ticks, randomize spawnTick++; var spawnInterval = 40 + Math.floor(Math.random() * 20); if (spawnTick > spawnInterval) { spawnObstacleOrEnemy(); spawnTick = 0; } // Update enemy cars for (var i = enemyCars.length - 1; i >= 0; i--) { var enemy = enemyCars[i]; enemy.update(); // Off screen if (enemy.y > 2732 + 300) { enemy.destroy(); enemyCars.splice(i, 1); updateScore(score + 1); continue; } // Collision with spell bottle if (spellBottle && enemy.intersects(spellBottle)) { spellBottle.destroy(); spellBottle = null; spellBottleActive = false; } // Collision with player if (enemy.intersects(playerCar)) { if (playerCar.shieldActive) { // Ignore collision, let player pass through enemy car while shield is active continue; } else { // --- LIFE SYSTEM: lose 1 life, give 1 to enemy, show bar --- if (playerLife > 0) { playerLife--; playerCar.life = playerLife; // Give 1 life to enemy (could be used for enemy powerup, here just for demo) if (!enemy.life) enemy.life = 0; enemy.life++; // If enemy is a fireball, update its health bar if (typeof enemy.updateLifeBar === "function") { enemy.updateLifeBar(); } updateLifeBar(); LK.effects.flashObject(playerCar, 0xff0000, 400); if (playerLife <= 0) { LK.getSound('crash').play(); LK.effects.flashScreen(0xff0000, 800); // Explosion effect at player car position LK.effects.flashObject(playerCar, 0xffffff, 700); LK.showGameOver(); return; } // Remove enemy on hit enemy.destroy(); enemyCars.splice(i, 1); continue; } else { LK.getSound('crash').play(); LK.effects.flashScreen(0xff0000, 800); // Explosion effect at player car position LK.effects.flashObject(playerCar, 0xffffff, 700); LK.showGameOver(); return; } } } } // Update obstacles for (var j = obstacles.length - 1; j >= 0; j--) { var obs = obstacles[j]; obs.update(); // Off screen if (obs.y > 2732 + 200) { obs.destroy(); obstacles.splice(j, 1); updateScore(score + 1); continue; } // Collision with player if (obs.intersects(playerCar)) { if (playerCar.shieldActive) { // Ignore collision, let player pass through obstacle while shield is active continue; } else { LK.getSound('crash').play(); LK.effects.flashScreen(0xff0000, 800); // Explosion effect at player car position LK.effects.flashObject(playerCar, 0xffffff, 700); LK.showGameOver(); return; } } } // Update fireballs if (!window.fireballs) window.fireballs = []; for (var f = window.fireballs.length - 1; f >= 0; f--) { var fireball = window.fireballs[f]; fireball.update(); // Track lastY for fireball to detect when it leaves the screen if (typeof fireball.lastY === "undefined") fireball.lastY = fireball.y; // Track if player has already passed this fireball for scoring if (typeof fireball.passedByPlayer === "undefined") fireball.passedByPlayer = false; // Award points if player passes by (dodges) the fireball // Player "passes by" if fireball moves below the player car, and they did not collide if (!fireball.passedByPlayer && fireball.lastY <= playerCar.y && fireball.y > playerCar.y && fireball.life > 0 // Only if not destroyed ) { updateScore(score + 2); // Award 2 points for dodging a fireball fireball.passedByPlayer = true; } // Off screen if (fireball.y > 2732 + 200) { fireball.destroy(); window.fireballs.splice(f, 1); continue; } // Award points if fireball is destroyed (life reaches 0) if (typeof fireball.lastLife === "undefined") fireball.lastLife = fireball.life; if (fireball.lastLife > 0 && fireball.life <= 0) { // Fireball was just destroyed this frame // Always award points for destroying a fireball, even after 70 points updateScore(score + 3); // Award 3 points for destroying a fireball fireball.destroy(); window.fireballs.splice(f, 1); continue; } fireball.lastLife = fireball.life; // Collision with player if (fireball.intersects(playerCar)) { if (playerCar.shieldActive) { // Ignore collision, let player pass through fireball while shield is active // Do NOT destroy the fireball, just let it continue continue; } else { LK.getSound('crash').play(); LK.effects.flashScreen(0xff3300, 900); LK.effects.flashObject(playerCar, 0xff6600, 700); LK.showGameOver(); return; } } fireball.lastY = fireball.y; } // --- HELLBOX REMOVED --- ; // Shield timer logic if (playerCar.shieldActive) { playerCar.shieldTicks--; // Show shield seconds left (rounded up) var shieldSeconds = Math.ceil(playerCar.shieldTicks / 60); shieldTxt.setText("Shield: " + shieldSeconds + "s"); // Show shield effect shieldEffect.visible = true; // Keep shield effect centered on playerCar shieldEffect.x = playerCar.x; shieldEffect.y = playerCar.y; // Let the shield effect rotate smoothly around itself using tween if (typeof shieldEffect._tweening === "undefined" || !shieldEffect._tweening) { var _spinShield = function spinShield() { tween(shieldEffect, { rotation: shieldEffect.rotation + Math.PI * 2 }, { duration: 2200, easing: tween.linear, onFinish: _spinShield }); }; shieldEffect.rotation = 0; shieldEffect._tweening = true; _spinShield(); } if (playerCar.shieldTicks <= 0) { playerCar.shieldActive = false; playerCar.shieldTicks = 0; shieldTxt.setText(''); shieldEffect.visible = false; } } else { shieldTxt.setText(''); shieldEffect.visible = false; } // --- LIFE BAR UI update --- // Draw a simple, always-visible horizontal health bar above the player car if (!window.playerHealthBar) { // Bar background (expanded, taller) window.playerHealthBarBg = LK.getAsset('lifeBarBg', { anchorX: 0.5, anchorY: 0.5, width: 520, height: 120, alpha: 0.85 }); // Bar fill (expanded, taller) window.playerHealthBarFill = LK.getAsset('lifeBar', { anchorX: 0.0, anchorY: 0.5, width: 480, height: 90, x: -240, alpha: 1 }); // Bar border (expanded, taller) window.playerHealthBarBorder = LK.getAsset('lifeBarBorder', { anchorX: 0.5, anchorY: 0.5, width: 560, height: 130, alpha: 1 }); // Add to game game.addChild(window.playerHealthBarBg); game.addChild(window.playerHealthBarFill); game.addChild(window.playerHealthBarBorder); window.playerHealthBar = true; } // Always show the health bar unless dead var showBar = playerLife > 0; window.playerHealthBarBg.visible = showBar; window.playerHealthBarFill.visible = showBar; window.playerHealthBarBorder.visible = showBar; // Position above player car if (playerCar) { var barY = (typeof playerCar.baseY !== "undefined" ? playerCar.baseY : playerCar.y) - 140; var barX = playerCar.x; window.playerHealthBarBg.x = barX; window.playerHealthBarBg.y = barY; window.playerHealthBarFill.x = barX - 240; window.playerHealthBarFill.y = barY; window.playerHealthBarBorder.x = barX; window.playerHealthBarBorder.y = barY; // Set width proportional to life, always show a minimum width if not dead var minWidth = 38; var fillW = playerLife > 0 ? Math.max(minWidth, Math.round(480 * (playerLife / playerMaxLife))) : 0; window.playerHealthBarFill.width = fillW; } // Hide if dead if (playerLife <= 0) { window.playerHealthBarBg.visible = false; window.playerHealthBarFill.visible = false; window.playerHealthBarBorder.visible = false; } }; // Place high score text to the left of the score text, both horizontally aligned highScoreTxt.x = LK.gui.top.width / 2 - 220; highScoreTxt.y = scoreTxt.height / 2 + 10; scoreTxt.x = LK.gui.top.width / 2 - 60; scoreTxt.y = scoreTxt.height / 2 + 10; // Position shield timer text directly below score, moved a little to the left shieldTxt.x = LK.gui.top.width / 2 - 80; shieldTxt.y = scoreTxt.y + scoreTxt.height / 2 + shieldTxt.height / 2 + 10; ;
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Enemy Car Class
// Note: The car sprite is always the first child (self.children[0]) for tinting purposes.
var EnemyCar = Container.expand(function () {
var self = Container.call(this);
var car = self.attachAsset('enemyCar', {
anchorX: 0.5,
anchorY: 0.5
});
// --- Overlay a white rectangle for the window ---
// The window is always in the same place on the enemyCar asset, so we overlay a white box.
// These values are tuned for the asset's window position and size.
var windowOverlay = LK.getAsset('heartIcon', {
anchorX: 0.5,
anchorY: 0.5,
width: 70,
height: 38,
x: 0,
y: -38,
tint: 0xffffff,
alpha: 1
});
self.addChild(windowOverlay);
self.lane = 1;
self.speed = 18;
self.update = function () {
self.y += self.speed * gameSpeed;
};
return self;
});
// Fireball Class
var Fireball = Container.expand(function () {
var self = Container.call(this);
var fireball = self.attachAsset('Fireball', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 28;
self.lane = 1;
// --- Health bar for Fireball (ace) ---
self.maxLife = 3;
self.life = self.maxLife;
// Health bar background
self.lifeBarBg = self.attachAsset('lifeBarBg', {
anchorX: 0.5,
anchorY: 0.5,
width: 220,
height: 220,
y: -90,
alpha: 0.92
});
// Health bar border
self.lifeBarBorder = self.attachAsset('lifeBarBorder', {
anchorX: 0.5,
anchorY: 0.5,
width: 260,
height: 260,
y: -90,
alpha: 1
});
// Health bar fill
self.lifeBar = self.attachAsset('lifeBar', {
anchorX: 0.0,
anchorY: 0.5,
width: 180,
height: 60,
x: -90,
y: -90,
alpha: 1
});
// Heart icon
self.lifeBarHeart = self.attachAsset('heartIcon', {
anchorX: 0.5,
anchorY: 0.5,
width: 70,
height: 70,
x: -130,
y: -90,
alpha: 1
});
// Helper to update fireball health bar UI
self.updateLifeBar = function () {
// Show only if not full and not dead
var show = self.life < self.maxLife && self.life > 0;
self.lifeBarBg.visible = show;
self.lifeBar.visible = show;
self.lifeBarBorder.visible = show;
self.lifeBarHeart.visible = show;
// Set width proportional to life
var minWidth = 24;
var w = self.life > 0 ? Math.max(minWidth, Math.round(180 * (self.life / self.maxLife))) : 0;
self.lifeBar.width = w;
// Hide if dead
if (self.life <= 0) {
self.lifeBarBg.visible = false;
self.lifeBar.visible = false;
self.lifeBarBorder.visible = false;
self.lifeBarHeart.visible = false;
}
};
// Initialize bar state
self.updateLifeBar();
self.update = function () {
// Move fireball down the screen
self.y += self.speed * gameSpeed;
// Keep health bar above fireball
self.lifeBarBg.y = -90;
self.lifeBarBorder.y = -90;
self.lifeBar.y = -90;
self.lifeBarHeart.y = -90;
self.lifeBar.x = -90;
self.lifeBarHeart.x = -130;
self.updateLifeBar();
};
return self;
});
// Lane Divider Class
var LaneDivider = Container.expand(function () {
var self = Container.call(this);
var div = self.attachAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 18;
self.update = function () {
self.y += self.speed * gameSpeed;
};
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.lane = 1;
self.speed = 18;
self.update = function () {
self.y += self.speed * gameSpeed;
};
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
});
self.lane = 1; // 0: left, 1: center, 2: right
self.life = playerMaxLife; // Add life property to player car
self.setLane = function (laneIdx) {
self.lane = laneIdx;
// Animate to new lane position
var targetX = lanes[self.lane];
// Always reset Y to baseY for lane change
if (typeof self.baseY === "undefined") self.baseY = 2732 - 500;
tween.stop(self, {
scaleX: true,
scaleY: true,
y: true
});
// Squash/stretch effect: squash horizontally, stretch vertically, then restore
tween(self, {
scaleX: 1.18,
scaleY: 0.88
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 80,
easing: tween.cubicOut
});
}
});
// Move to new lane and reset Y to baseY (so bounce resumes from correct position)
tween(self, {
x: targetX,
y: self.baseY
}, {
duration: 120,
easing: tween.cubicOut,
onUpdate: function onUpdate() {
updateLifeBar();
}
});
updateLifeBar();
};
// Idle bounce animation for player car
self.baseY = 2732 - 500; // Store the base Y for idle bounce
self.idleBounce = function () {
// Cancel any previous bounce
tween.stop(self, {
y: true
});
// Animate up
tween(self, {
y: self.baseY - 18
}, {
duration: 320,
easing: tween.sineInOut,
onFinish: function onFinish() {
// Animate down
tween(self, {
y: self.baseY + 18
}, {
duration: 320,
easing: tween.sineInOut,
onFinish: function onFinish() {
// Loop bounce
self.idleBounce();
}
});
}
});
};
// Start idle bounce when created
self.idleBounce();
// Clamp Y position to prevent slipping up
self.update = function () {
// If y is above the allowed minimum, clamp it back
var minY = 2732 - 700; // Don't let car go above this Y (adjust as needed)
if (self.y < minY) {
self.y = minY;
}
};
return self;
});
// --- PEOPLE WALKING ON SIDEWALKS ---
// Person class for sidewalk walkers
var SidewalkPerson = Container.expand(function () {
var self = Container.call(this);
// Use heartIcon as a placeholder for a person (circle/box)
var person = self.attachAsset('heartIcon', {
anchorX: 0.5,
anchorY: 0.5,
width: 60,
height: 60,
tint: 0x333333 + Math.floor(Math.random() * 0xCCCCCC),
// random grayish color
alpha: 1
});
// Add hands (arms) as rectangles
var leftHand = LK.getAsset('heartIcon', {
anchorX: 0.5,
anchorY: 0.5,
width: 16,
height: 38,
x: -32,
y: 18,
tint: 0xdeb887,
// light brown, can randomize for variety
alpha: 1
});
var rightHand = LK.getAsset('heartIcon', {
anchorX: 0.5,
anchorY: 0.5,
width: 16,
height: 38,
x: 32,
y: 18,
tint: 0xdeb887,
alpha: 1
});
self.addChild(leftHand);
self.addChild(rightHand);
// Randomize direction: 1 = down, -1 = up
self.dir = Math.random() < 0.5 ? 1 : -1;
// Set speed (pixels per frame)
self.speed = 4 + Math.random() * 2;
// Set sidewalk: 0 = left, 1 = right
self.side = 0;
// Set initial position
if (Math.random() < 0.5) {
self.side = 0;
self.x = sidewalkWidth / 2;
} else {
self.side = 1;
self.x = 2048 - sidewalkWidth / 2;
}
// Y: if going down, start at top; if up, start at bottom
self.y = self.dir === 1 ? -40 - Math.random() * 200 : 2732 + 40 + Math.random() * 200;
// Sway animation
self.swayTick = Math.random() * Math.PI * 2;
self.update = function () {
// Move up or down
self.y += self.speed * self.dir;
// Sway left/right a little
self.swayTick += 0.08;
var sway = Math.sin(self.swayTick) * 10;
self.x = (self.side === 0 ? sidewalkWidth / 2 : 2048 - sidewalkWidth / 2) + sway;
// --- Magic bottle in hand logic ---
if (typeof self.handBottle === "undefined") {
self.handBottle = null;
}
if (typeof self.craveSpellBottle === "undefined") self.craveSpellBottle = Math.random() < 0.5; // 50% crave
// If craving and not on cooldown, show bottle in hand
if (self.craveSpellBottle && self.throwCooldown <= 0 && !self.handBottle) {
// Add a bottle in the hand (side dependent)
self.handBottle = LK.getAsset('spellBottle', {
anchorX: 0.5,
anchorY: 0.5,
width: 38,
height: 38,
x: self.side === 0 ? -32 : 32,
// left or right hand
y: 18,
tint: 0x2BD5E6,
alpha: 1
});
self.addChild(self.handBottle);
}
// If not craving or on cooldown, remove bottle from hand if present
if ((!self.craveSpellBottle || self.throwCooldown > 0) && self.handBottle) {
self.handBottle.destroy();
self.handBottle = null;
}
};
return self;
});
/****
* Initialize Game
****/
// Create and track sidewalk people
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// Use spellBottle as heart
// Health bar assets
// Spell bottle asset (example: blue bottle, 120x180)
// When the player picks up the magic bottle, the car bursts into flames (flame burst effect)
// Lane positions (3 lanes)
// Car (player)
// Enemy car
// Obstacle (barrier)
// Road lane divider
// Sound for crash
// Sound for lane change
// Music (background)
// Person drawing asset for sidewalk walkers (customize this for identity)
// This is a simple stick-figure style: head (ellipse), body (box), arms (boxes), legs (boxes)
var laneCount = 3;
var laneWidth = 520; // Increased lane width for more space between lanes
var lanes = [2048 / 2 - laneWidth,
// left
2048 / 2,
// center
2048 / 2 + laneWidth // right
];
// Game variables
var playerCar;
var enemyCars = [];
var obstacles = [];
var laneDividers = [];
var score = 0;
var highScore = storage.highScore || 0;
var scoreTxt;
var highScoreTxt;
var gameSpeed = 1;
var ticksSinceStart = 0;
var swipeStartX = null;
var swipeStartY = null;
var swipeActive = false;
var lastLane = 1;
var spawnTick = 0;
var dividerSpacing = 320;
var dragNode = null;
// --- LANE IDLE TIMER SYSTEM ---
var laneIdleTicks = [0, 0, 0]; // Track how long player stays in each lane (in ticks)
var lastPlayerLane = 1; // Track last lane for idle timer
var fireballIdleTriggered = [false, false, false]; // Prevent multiple fireballs for same idle period
// --- LIFE SYSTEM ---
var playerMaxLife = 5;
var playerLife = playerMaxLife;
// Health bar UI will be handled by a new, clear, and consistent system below
// Score display
scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0.5);
LK.gui.top.addChild(scoreTxt);
// Shield timer display
var shieldTxt = new Text2('', {
size: 80,
fill: 0x2BD5E6
});
shieldTxt.anchor.set(0.5, 0.5);
LK.gui.top.addChild(shieldTxt);
// High Score display
highScoreTxt = new Text2('HI: ' + highScore, {
size: 60,
fill: 0xFFFF00
});
highScoreTxt.anchor.set(0.5, 0.5);
LK.gui.top.addChild(highScoreTxt);
// Add swipe left gesture to high score text
var hiSwipeStartX = null;
var hiSwipeStartY = null;
var hiSwipeActive = false;
highScoreTxt.down = function (x, y, obj) {
hiSwipeStartX = x;
hiSwipeStartY = y;
hiSwipeActive = true;
};
highScoreTxt.move = function (x, y, obj) {
if (!hiSwipeActive) return;
var dx = x - hiSwipeStartX;
var dy = y - hiSwipeStartY;
// Only consider horizontal swipes, ignore vertical
if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy)) {
if (dx < 0) {
// Swipe left detected on high score text
// Flash the high score text white for feedback
tween(highScoreTxt, {
tint: 0xffffff
}, {
duration: 80,
yoyo: true,
repeat: 1,
onComplete: function onComplete() {
highScoreTxt.tint = 0xFFFF00;
}
});
hiSwipeActive = false;
}
}
};
highScoreTxt.up = function (x, y, obj) {
hiSwipeActive = false;
};
// Start music
LK.playMusic('bgmusic');
// Create player car
playerCar = new PlayerCar();
playerCar.x = lanes[1];
playerCar.y = 2732 - 500;
playerCar.setLane(1);
// Shield properties
playerCar.shieldActive = false;
playerCar.shieldTicks = 0;
// Add shield effect asset (invisible by default)
var shieldEffect = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.2,
scaleY: 1.2,
x: playerCar.x,
y: playerCar.y,
alpha: 0.5,
tint: 0xffffff
});
shieldEffect.visible = false;
game.addChild(shieldEffect);
game.addChild(playerCar);
// Add sidewalks to the far left and right edges of the screen (do not touch the road)
var sidewalkWidth = 120;
var sidewalkHeight = 2732;
var sidewalkColor = 0xcccccc; // Light gray for sidewalk
var leftSidewalk = LK.getAsset('heartIcon', {
anchorX: 0,
anchorY: 0,
width: sidewalkWidth,
height: sidewalkHeight,
x: 0,
y: 0,
tint: sidewalkColor,
alpha: 1
});
var rightSidewalk = LK.getAsset('heartIcon', {
anchorX: 0,
anchorY: 0,
width: sidewalkWidth,
height: sidewalkHeight,
x: 2048 - sidewalkWidth,
y: 0,
tint: sidewalkColor,
alpha: 1
});
game.addChild(leftSidewalk);
game.addChild(rightSidewalk);
// Create and track sidewalk people
var sidewalkPeople = [];
var maxPeoplePerSide = 6;
for (var i = 0; i < maxPeoplePerSide * 2; i++) {
var p = new SidewalkPerson();
sidewalkPeople.push(p);
game.addChild(p);
}
// Create lane dividers (vertical lines for each lane)
// For 3 lanes, we want to copy the center lane divider and place it to the right and left of the center stripe, without adding extra lines
// The center divider is between lane 0 and lane 1, and between lane 1 and lane 2
// So, for 3 lanes, we want to draw the two dividers: one between lane 0 and 1, and one between lane 1 and 2
var dividerOffsets = [];
// Find the X positions for the two dividers (between lanes)
for (var l = 1; l < laneCount; l++) {
dividerOffsets.push((lanes[l - 1] + lanes[l]) / 2);
}
// Now, for each divider, draw a set of stripes down the screen
for (var d = 0; d < dividerOffsets.length; d++) {
for (var i = 0; i < 10; i++) {
// Center LaneDivider
var divider = new LaneDivider();
divider.x = dividerOffsets[d];
divider.y = i * dividerSpacing;
laneDividers.push(divider);
game.addChild(divider);
// Left copy
var dividerLeft = new LaneDivider();
dividerLeft.x = dividerOffsets[d] - laneWidth;
dividerLeft.y = i * dividerSpacing;
laneDividers.push(dividerLeft);
game.addChild(dividerLeft);
// Right copy
var dividerRight = new LaneDivider();
dividerRight.x = dividerOffsets[d] + laneWidth;
dividerRight.y = i * dividerSpacing;
laneDividers.push(dividerRight);
game.addChild(dividerRight);
}
}
// Prepare for dynamic spell bottle spawning
var spellBottle = null;
var spellBottleActive = false;
var spellBottleLane = 1;
// Add: track last spell bottle spawn tick to limit spawn rate
var lastSpellBottleSpawnTick = -1000;
// Helper: spawn enemy car, obstacle, or fireball
function spawnObstacleOrEnemy() {
var y = -300;
if (score >= 400) {
// After 400 points, restore original spawn logic but with higher speed (handled by gameSpeed)
// Randomly decide: 60% enemy car, 25% obstacle, 15% fireball
var laneIdx = Math.floor(Math.random() * laneCount);
var rand = Math.random();
if (rand < 0.6) {
var enemy = new EnemyCar();
enemy.lane = laneIdx;
enemy.x = lanes[laneIdx];
enemy.y = y;
// Assign a bold, high-contrast color tint to the enemy car for visibility
var enemyTints = [0xFFFFFF, 0xFF3333, 0x33CCFF, 0xFFCC00, 0x00FF66, 0xFF66FF, 0xFF8800, 0x9933FF];
var randomColor = enemyTints[Math.floor(Math.random() * enemyTints.length)];
if (enemy.children && enemy.children.length > 0) {
enemy.children[0].tint = randomColor;
}
enemyCars.push(enemy);
game.addChild(enemy);
} else if (rand < 0.85) {
var obs = new Obstacle();
obs.lane = laneIdx;
obs.x = lanes[laneIdx];
obs.y = y;
obstacles.push(obs);
game.addChild(obs);
} else {
// Prevent fireballs from spawning before 60 points
if (score >= 60) {
// Limit fireballs: only spawn if fewer than 2 are on screen
if (!window.fireballs) window.fireballs = [];
if (window.fireballs.length < 2) {
var fireball = new Fireball();
fireball.lane = laneIdx;
fireball.x = lanes[laneIdx];
fireball.y = y;
window.fireballs.push(fireball);
game.addChild(fireball);
}
}
}
} else if (score > 200) {
// After 200 points, only spawn barriers (obstacles), and spawn in 2 lanes at once
// Pick two different lanes
var lanesToUse = [];
while (lanesToUse.length < 2) {
var idx = Math.floor(Math.random() * laneCount);
if (lanesToUse.indexOf(idx) === -1) lanesToUse.push(idx);
}
for (var i = 0; i < lanesToUse.length; i++) {
var obs = new Obstacle();
obs.lane = lanesToUse[i];
obs.x = lanes[lanesToUse[i]];
obs.y = y;
obstacles.push(obs);
game.addChild(obs);
}
} else if (score >= 70) {
// Only spawn fireballs after 70 points
if (!window.fireballs) window.fireballs = [];
if (window.fireballs.length < 2) {
var laneIdx = Math.floor(Math.random() * laneCount);
var fireball = new Fireball();
fireball.lane = laneIdx;
fireball.x = lanes[laneIdx];
fireball.y = y;
window.fireballs.push(fireball);
game.addChild(fireball);
}
} else {
// Randomly decide: 60% enemy car, 25% obstacle, 15% fireball
var laneIdx = Math.floor(Math.random() * laneCount);
var rand = Math.random();
if (rand < 0.6) {
var enemy = new EnemyCar();
enemy.lane = laneIdx;
enemy.x = lanes[laneIdx];
enemy.y = y;
// Assign a bold, high-contrast color tint to the enemy car for visibility
// Use a set of bold, high-contrast tints for random color
var enemyTints = [0xFFFFFF,
// white
0xFF3333,
// red
0x33CCFF,
// blue
0xFFCC00,
// yellow
0x00FF66,
// green
0xFF66FF,
// magenta
0xFF8800,
// orange
0x9933FF // purple
];
var randomColor = enemyTints[Math.floor(Math.random() * enemyTints.length)];
if (enemy.children && enemy.children.length > 0) {
enemy.children[0].tint = randomColor;
}
enemyCars.push(enemy);
game.addChild(enemy);
} else if (rand < 0.85) {
var obs = new Obstacle();
obs.lane = laneIdx;
obs.x = lanes[laneIdx];
obs.y = y;
obstacles.push(obs);
game.addChild(obs);
} else {
// Prevent fireballs from spawning before 60 points
if (score >= 60) {
// Limit fireballs: only spawn if fewer than 2 are on screen
if (!window.fireballs) window.fireballs = [];
if (window.fireballs.length < 2) {
var fireball = new Fireball();
fireball.lane = laneIdx;
fireball.x = lanes[laneIdx];
fireball.y = y;
window.fireballs.push(fireball);
game.addChild(fireball);
}
}
}
}
}
// Helper: update score
function updateScore(val) {
score = val;
scoreTxt.setText('' + score); // Instantly update score text
if (score > highScore) {
highScore = score;
highScoreTxt.setText('HI: ' + highScore);
storage.highScore = highScore;
}
}
// Helper: update player health bar UI (global, for use in PlayerCar.setLane and elsewhere)
function updateLifeBar() {
// Always show the health bar unless dead
var showBar = playerLife > 0;
if (window.playerHealthBarBg) window.playerHealthBarBg.visible = showBar;
if (window.playerHealthBarFill) window.playerHealthBarFill.visible = showBar;
if (window.playerHealthBarBorder) window.playerHealthBarBorder.visible = showBar;
// Removed: if (window.playerHealthBarHeart) window.playerHealthBarHeart.visible = showBar;
// Position above player car
if (playerCar && window.playerHealthBarBg && window.playerHealthBarFill && window.playerHealthBarBorder) {
var barY = playerCar.y - 140;
var barX = playerCar.x;
window.playerHealthBarBg.x = barX;
window.playerHealthBarBg.y = barY;
window.playerHealthBarFill.x = barX - 240;
window.playerHealthBarFill.y = barY;
window.playerHealthBarBorder.x = barX;
window.playerHealthBarBorder.y = barY;
// Removed: window.playerHealthBarHeart.x = barX - 320;
// Removed: window.playerHealthBarHeart.y = barY;
// Set width proportional to life, always show a minimum width if not dead
var minWidth = 38;
var fillW = playerLife > 0 ? Math.max(minWidth, Math.round(480 * (playerLife / playerMaxLife))) : 0;
window.playerHealthBarFill.width = fillW;
}
// Hide if dead
if (playerLife <= 0) {
if (window.playerHealthBarBg) window.playerHealthBarBg.visible = false;
if (window.playerHealthBarFill) window.playerHealthBarFill.visible = false;
if (window.playerHealthBarBorder) window.playerHealthBarBorder.visible = false;
// Removed: if (window.playerHealthBarHeart) window.playerHealthBarHeart.visible = false;
}
}
// Touch/drag/swipe handling
game.down = function (x, y, obj) {
swipeStartX = x;
swipeStartY = y;
swipeActive = true;
dragNode = playerCar;
};
game.move = function (x, y, obj) {
// Only handle swipe if active
if (!swipeActive) return;
if (!dragNode) return;
var dx = x - swipeStartX;
var dy = y - swipeStartY;
// Only consider horizontal swipes, ignore vertical
if (Math.abs(dx) > 80 && Math.abs(dx) > Math.abs(dy)) {
var newLane = playerCar.lane;
if (dx < 0 && playerCar.lane > 0) {
newLane = playerCar.lane - 1;
} else if (dx > 0 && playerCar.lane < laneCount - 1) {
newLane = playerCar.lane + 1;
}
if (newLane !== playerCar.lane) {
// Reset idle timer for previous lane, and for new lane
laneIdleTicks[playerCar.lane] = 0;
fireballIdleTriggered[playerCar.lane] = false;
playerCar.setLane(newLane);
// Also reset for new lane
laneIdleTicks[newLane] = 0;
fireballIdleTriggered[newLane] = false;
lastPlayerLane = newLane;
LK.getSound('swipe').play();
swipeActive = false;
dragNode = null;
}
}
};
game.up = function (x, y, obj) {
swipeActive = false;
dragNode = null;
};
// Main game update loop
game.update = function () {
ticksSinceStart++;
// --- LANE IDLE TIMER UPDATE ---
// Only increment for the current lane, reset others
for (var l = 0; l < laneCount; l++) {
if (l === playerCar.lane) {
laneIdleTicks[l]++;
} else {
laneIdleTicks[l] = 0;
fireballIdleTriggered[l] = false;
}
}
// If player has stayed in the same lane for >10s (600 ticks), spawn a fireball in that lane
if (laneIdleTicks[playerCar.lane] > 600 && !fireballIdleTriggered[playerCar.lane]) {
// Only spawn if fewer than 2 fireballs are on screen
if (!window.fireballs) window.fireballs = [];
if (window.fireballs.length < 2) {
var fireball = new Fireball();
fireball.lane = playerCar.lane;
fireball.x = lanes[playerCar.lane];
fireball.y = -300;
window.fireballs.push(fireball);
game.addChild(fireball);
}
fireballIdleTriggered[playerCar.lane] = true;
// Optionally, flash a warning or effect here if desired
}
// Adjust acceleration: start at 20 points, increase slowly until 70 points, then ramp up
if (score >= 400) {
// After 400 points, restore original spawn logic but make everything much faster
gameSpeed = 2.5; // Significantly faster than normal
} else if (score >= 250) {
// Between 250 and 400, slow down the speed a little (decelerate)
if (ticksSinceStart % 60 === 0 && gameSpeed > 1.2) {
gameSpeed -= 0.04;
if (gameSpeed < 1.2) gameSpeed = 1.2;
}
} else if (score >= 70) {
// After 70, only fireballs spawn and game returns to normal speed
gameSpeed = 1;
} else if (score >= 20) {
// Between 20 and 70, increase very slowly
if (ticksSinceStart % 90 === 0 && gameSpeed < 2) {
gameSpeed += 0.04;
if (gameSpeed > 2) gameSpeed = 2;
}
} else {
gameSpeed = 1;
}
// --- UPDATE SIDEWALK PEOPLE ---
for (var i = 0; i < sidewalkPeople.length; i++) {
var p = sidewalkPeople[i];
p.update();
// --- GIVE ACETITY: random chance to get a speed boost and color flash ---
if (typeof p.acetityCooldown === "undefined") p.acetityCooldown = 0;
if (typeof p.acetityActive === "undefined") p.acetityActive = false;
if (typeof p.baseSpeed === "undefined") p.baseSpeed = p.speed;
// --- THROWING ITEMS: only crave (desire) spell bottles, and rarely throw them ---
if (typeof p.throwCooldown === "undefined") p.throwCooldown = 0;
if (typeof p.thrownItems === "undefined") p.thrownItems = [];
if (typeof p.craveSpellBottle === "undefined") p.craveSpellBottle = Math.random() < 0.5; // 50% crave
if (p.throwCooldown > 0) {
p.throwCooldown--;
} else if (p.craveSpellBottle && (score < 70 && Math.random() < 0.003 ||
// 0.3% chance per frame before 70 points
score >= 70 && score < 400 && Math.random() < 0.001 ||
// 0.1% chance per frame after 70 points
score >= 400 && Math.random() < 0.025 // 2.5% chance per frame after 400 points (much more frequent)
) && score >= 30) {
// Only throw if not too close to top/bottom and not too many items already
if (p.y > 200 && p.y < 2732 - 200 && (!window.thrownItems || window.thrownItems.length < 2)) {
// Only throw spellBottle
var itemType = 'spellBottle';
var thrownItem = LK.getAsset(itemType, {
anchorX: 0.5,
anchorY: 0.5,
width: 80,
height: 80,
x: p.x + (p.side === 0 ? 60 : -60),
// throw toward road
y: p.y + 10,
tint: 0x2BD5E6,
alpha: 1
});
thrownItem._vx = (p.side === 0 ? 1 : -1) * (12 + Math.random() * 6); // horizontal speed toward road
thrownItem._vy = 10 + Math.random() * 6; // vertical speed
thrownItem._gravity = 0.7 + Math.random() * 0.2;
thrownItem._rotSpeed = (Math.random() - 0.5) * 0.2;
thrownItem._fromSide = p.side;
thrownItem._active = true;
if (!window.thrownItems) window.thrownItems = [];
window.thrownItems.push(thrownItem);
game.addChild(thrownItem);
// Remove bottle from hand immediately after throw
if (typeof p.handBottle !== "undefined" && p.handBottle) {
p.handBottle.destroy();
p.handBottle = null;
}
p.throwCooldown = 360 + Math.floor(Math.random() * 180); // 6-9s cooldown, much less frequent
}
}
// Cooldown for next acetity
if (p.acetityCooldown > 0) {
p.acetityCooldown--;
} else if (!p.acetityActive && Math.random() < 0.008) {
// 0.8% chance per frame to get acetity if not active
p.acetityActive = true;
p.acetityTicks = 60 + Math.floor(Math.random() * 60); // 1-2 seconds
p.baseSpeed = p.speed;
p.speed = p.baseSpeed * (1.7 + Math.random() * 0.7); // 1.7x-2.4x speed
// Flash color: yellow or cyan
if (p.children && p.children.length > 0) {
var flashColor = Math.random() < 0.5 ? 0xffff00 : 0x00ffff;
tween(p.children[0], {
tint: flashColor
}, {
duration: 80,
yoyo: true,
repeat: 2,
onComplete: function onComplete() {
if (p.children && p.children.length > 0) {
p.children[0].tint = 0x333333 + Math.floor(Math.random() * 0xCCCCCC);
}
}
});
}
}
// If acetity is active, count down and restore speed after
if (p.acetityActive) {
p.acetityTicks--;
// Optionally, make them slightly larger while acetity is active
if (p.children && p.children.length > 0) {
p.children[0].scaleX = 1.18;
p.children[0].scaleY = 1.18;
}
if (p.acetityTicks <= 0) {
p.acetityActive = false;
p.speed = p.baseSpeed;
p.acetityCooldown = 120 + Math.floor(Math.random() * 120); // 2-4s cooldown
if (p.children && p.children.length > 0) {
p.children[0].scaleX = 1;
p.children[0].scaleY = 1;
}
}
}
// If off screen, respawn at the other end with new speed/direction
if (p.dir === 1 && p.y > 2732 + 80 || p.dir === -1 && p.y < -80) {
// Randomize direction
p.dir = Math.random() < 0.5 ? 1 : -1;
// Randomize sidewalk
p.side = Math.random() < 0.5 ? 0 : 1;
p.x = p.side === 0 ? sidewalkWidth / 2 : 2048 - sidewalkWidth / 2;
// Y: if going down, start at top; if up, start at bottom
p.y = p.dir === 1 ? -40 - Math.random() * 200 : 2732 + 40 + Math.random() * 200;
// Randomize speed
p.speed = 4 + Math.random() * 2;
p.baseSpeed = p.speed;
p.acetityActive = false;
p.acetityCooldown = 0;
// Randomize color
if (p.children && p.children.length > 0) {
p.children[0].tint = 0x333333 + Math.floor(Math.random() * 0xCCCCCC);
p.children[0].scaleX = 1;
p.children[0].scaleY = 1;
}
// Randomize sway phase
p.swayTick = Math.random() * Math.PI * 2;
}
}
// Move lane dividers, loop to top
for (var i = 0; i < laneDividers.length; i++) {
var div = laneDividers[i];
div.update();
if (div.y > 2732 + 60) {
div.y -= dividerSpacing * 10;
}
}
// --- UPDATE THROWN ITEMS (from sidewalk people) ---
if (!window.thrownItems) window.thrownItems = [];
for (var ti = window.thrownItems.length - 1; ti >= 0; ti--) {
var item = window.thrownItems[ti];
if (!item._active) continue;
// Move item
item.x += item._vx;
item.y += item._vy;
item._vy += item._gravity;
item.rotation += item._rotSpeed;
// If item is off screen, destroy
if (item.x < -100 || item.x > 2048 + 100 || item.y > 2732 + 100) {
item.destroy();
window.thrownItems.splice(ti, 1);
continue;
}
// If item reaches the road area (not sidewalk), allow collision with player
var roadLeft = sidewalkWidth + 40;
var roadRight = 2048 - sidewalkWidth - 40;
if (item.x > roadLeft && item.x < roadRight && item.y > 0 && item.y < 2732) {
// Check collision with playerCar
if (playerCar && item.intersects(playerCar)) {
// If it's a spellBottle (all thrown items are), grant shield to player AND increase life bar
if (!playerCar.shieldActive) {
playerCar.shieldActive = true;
playerCar.shieldTicks = 180; // 3 seconds at 60fps
// Add flame burst effect: flashObject with orange/yellow color, then white
var fireMagic = LK.getAsset('spellBottle', {
anchorX: 0.5,
anchorY: 0.5,
x: playerCar.x,
y: playerCar.y,
scaleX: 1,
scaleY: 1,
alpha: 0.85,
tint: 0xff6600
});
game.addChild(fireMagic);
tween(fireMagic, {
scaleX: 2.2,
scaleY: 2.2,
alpha: 0
}, {
duration: 1000,
easing: tween.cubicOut,
onUpdate: function onUpdate() {
fireMagic.x = playerCar.x;
fireMagic.y = playerCar.y;
},
onComplete: function onComplete() {
fireMagic.destroy();
}
});
LK.effects.flashObject(playerCar, 0xffa500, 350);
LK.setTimeout(function () {
LK.effects.flashObject(playerCar, 0xffff00, 350);
}, 350);
}
// Always increase life bar by 1 (up to max) when collecting a thrown bottle
if (playerLife < playerMaxLife) {
playerLife++;
playerCar.life = playerLife;
updateLifeBar();
}
// Remove item after hit
item.destroy();
window.thrownItems.splice(ti, 1);
continue;
}
}
}
// Spell bottle no longer spawns on the road
spellBottle = null;
spellBottleActive = false;
// Spawn enemies/obstacles every 40-60 ticks, randomize
spawnTick++;
var spawnInterval = 40 + Math.floor(Math.random() * 20);
if (spawnTick > spawnInterval) {
spawnObstacleOrEnemy();
spawnTick = 0;
}
// Update enemy cars
for (var i = enemyCars.length - 1; i >= 0; i--) {
var enemy = enemyCars[i];
enemy.update();
// Off screen
if (enemy.y > 2732 + 300) {
enemy.destroy();
enemyCars.splice(i, 1);
updateScore(score + 1);
continue;
}
// Collision with spell bottle
if (spellBottle && enemy.intersects(spellBottle)) {
spellBottle.destroy();
spellBottle = null;
spellBottleActive = false;
}
// Collision with player
if (enemy.intersects(playerCar)) {
if (playerCar.shieldActive) {
// Ignore collision, let player pass through enemy car while shield is active
continue;
} else {
// --- LIFE SYSTEM: lose 1 life, give 1 to enemy, show bar ---
if (playerLife > 0) {
playerLife--;
playerCar.life = playerLife;
// Give 1 life to enemy (could be used for enemy powerup, here just for demo)
if (!enemy.life) enemy.life = 0;
enemy.life++;
// If enemy is a fireball, update its health bar
if (typeof enemy.updateLifeBar === "function") {
enemy.updateLifeBar();
}
updateLifeBar();
LK.effects.flashObject(playerCar, 0xff0000, 400);
if (playerLife <= 0) {
LK.getSound('crash').play();
LK.effects.flashScreen(0xff0000, 800);
// Explosion effect at player car position
LK.effects.flashObject(playerCar, 0xffffff, 700);
LK.showGameOver();
return;
}
// Remove enemy on hit
enemy.destroy();
enemyCars.splice(i, 1);
continue;
} else {
LK.getSound('crash').play();
LK.effects.flashScreen(0xff0000, 800);
// Explosion effect at player car position
LK.effects.flashObject(playerCar, 0xffffff, 700);
LK.showGameOver();
return;
}
}
}
}
// Update obstacles
for (var j = obstacles.length - 1; j >= 0; j--) {
var obs = obstacles[j];
obs.update();
// Off screen
if (obs.y > 2732 + 200) {
obs.destroy();
obstacles.splice(j, 1);
updateScore(score + 1);
continue;
}
// Collision with player
if (obs.intersects(playerCar)) {
if (playerCar.shieldActive) {
// Ignore collision, let player pass through obstacle while shield is active
continue;
} else {
LK.getSound('crash').play();
LK.effects.flashScreen(0xff0000, 800);
// Explosion effect at player car position
LK.effects.flashObject(playerCar, 0xffffff, 700);
LK.showGameOver();
return;
}
}
}
// Update fireballs
if (!window.fireballs) window.fireballs = [];
for (var f = window.fireballs.length - 1; f >= 0; f--) {
var fireball = window.fireballs[f];
fireball.update();
// Track lastY for fireball to detect when it leaves the screen
if (typeof fireball.lastY === "undefined") fireball.lastY = fireball.y;
// Track if player has already passed this fireball for scoring
if (typeof fireball.passedByPlayer === "undefined") fireball.passedByPlayer = false;
// Award points if player passes by (dodges) the fireball
// Player "passes by" if fireball moves below the player car, and they did not collide
if (!fireball.passedByPlayer && fireball.lastY <= playerCar.y && fireball.y > playerCar.y && fireball.life > 0 // Only if not destroyed
) {
updateScore(score + 2); // Award 2 points for dodging a fireball
fireball.passedByPlayer = true;
}
// Off screen
if (fireball.y > 2732 + 200) {
fireball.destroy();
window.fireballs.splice(f, 1);
continue;
}
// Award points if fireball is destroyed (life reaches 0)
if (typeof fireball.lastLife === "undefined") fireball.lastLife = fireball.life;
if (fireball.lastLife > 0 && fireball.life <= 0) {
// Fireball was just destroyed this frame
// Always award points for destroying a fireball, even after 70 points
updateScore(score + 3); // Award 3 points for destroying a fireball
fireball.destroy();
window.fireballs.splice(f, 1);
continue;
}
fireball.lastLife = fireball.life;
// Collision with player
if (fireball.intersects(playerCar)) {
if (playerCar.shieldActive) {
// Ignore collision, let player pass through fireball while shield is active
// Do NOT destroy the fireball, just let it continue
continue;
} else {
LK.getSound('crash').play();
LK.effects.flashScreen(0xff3300, 900);
LK.effects.flashObject(playerCar, 0xff6600, 700);
LK.showGameOver();
return;
}
}
fireball.lastY = fireball.y;
}
// --- HELLBOX REMOVED ---
;
// Shield timer logic
if (playerCar.shieldActive) {
playerCar.shieldTicks--;
// Show shield seconds left (rounded up)
var shieldSeconds = Math.ceil(playerCar.shieldTicks / 60);
shieldTxt.setText("Shield: " + shieldSeconds + "s");
// Show shield effect
shieldEffect.visible = true;
// Keep shield effect centered on playerCar
shieldEffect.x = playerCar.x;
shieldEffect.y = playerCar.y;
// Let the shield effect rotate smoothly around itself using tween
if (typeof shieldEffect._tweening === "undefined" || !shieldEffect._tweening) {
var _spinShield = function spinShield() {
tween(shieldEffect, {
rotation: shieldEffect.rotation + Math.PI * 2
}, {
duration: 2200,
easing: tween.linear,
onFinish: _spinShield
});
};
shieldEffect.rotation = 0;
shieldEffect._tweening = true;
_spinShield();
}
if (playerCar.shieldTicks <= 0) {
playerCar.shieldActive = false;
playerCar.shieldTicks = 0;
shieldTxt.setText('');
shieldEffect.visible = false;
}
} else {
shieldTxt.setText('');
shieldEffect.visible = false;
}
// --- LIFE BAR UI update ---
// Draw a simple, always-visible horizontal health bar above the player car
if (!window.playerHealthBar) {
// Bar background (expanded, taller)
window.playerHealthBarBg = LK.getAsset('lifeBarBg', {
anchorX: 0.5,
anchorY: 0.5,
width: 520,
height: 120,
alpha: 0.85
});
// Bar fill (expanded, taller)
window.playerHealthBarFill = LK.getAsset('lifeBar', {
anchorX: 0.0,
anchorY: 0.5,
width: 480,
height: 90,
x: -240,
alpha: 1
});
// Bar border (expanded, taller)
window.playerHealthBarBorder = LK.getAsset('lifeBarBorder', {
anchorX: 0.5,
anchorY: 0.5,
width: 560,
height: 130,
alpha: 1
});
// Add to game
game.addChild(window.playerHealthBarBg);
game.addChild(window.playerHealthBarFill);
game.addChild(window.playerHealthBarBorder);
window.playerHealthBar = true;
}
// Always show the health bar unless dead
var showBar = playerLife > 0;
window.playerHealthBarBg.visible = showBar;
window.playerHealthBarFill.visible = showBar;
window.playerHealthBarBorder.visible = showBar;
// Position above player car
if (playerCar) {
var barY = (typeof playerCar.baseY !== "undefined" ? playerCar.baseY : playerCar.y) - 140;
var barX = playerCar.x;
window.playerHealthBarBg.x = barX;
window.playerHealthBarBg.y = barY;
window.playerHealthBarFill.x = barX - 240;
window.playerHealthBarFill.y = barY;
window.playerHealthBarBorder.x = barX;
window.playerHealthBarBorder.y = barY;
// Set width proportional to life, always show a minimum width if not dead
var minWidth = 38;
var fillW = playerLife > 0 ? Math.max(minWidth, Math.round(480 * (playerLife / playerMaxLife))) : 0;
window.playerHealthBarFill.width = fillW;
}
// Hide if dead
if (playerLife <= 0) {
window.playerHealthBarBg.visible = false;
window.playerHealthBarFill.visible = false;
window.playerHealthBarBorder.visible = false;
}
};
// Place high score text to the left of the score text, both horizontally aligned
highScoreTxt.x = LK.gui.top.width / 2 - 220;
highScoreTxt.y = scoreTxt.height / 2 + 10;
scoreTxt.x = LK.gui.top.width / 2 - 60;
scoreTxt.y = scoreTxt.height / 2 + 10;
// Position shield timer text directly below score, moved a little to the left
shieldTxt.x = LK.gui.top.width / 2 - 80;
shieldTxt.y = scoreTxt.y + scoreTxt.height / 2 + shieldTxt.height / 2 + 10;
;