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