/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Coin class
var Coin = Container.expand(function () {
var self = Container.call(this);
// Attach asset (ellipse for now)
var coinAsset = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5
});
coinAsset.width = 90;
coinAsset.height = 90;
// Animation: simple up-down bob
self.bobPhase = Math.random() * Math.PI * 2;
self.update = function () {
self.x -= gameSpeed;
coinAsset.y = Math.sin(LK.ticks * 0.15 + self.bobPhase) * 12;
};
return self;
});
// Obstacle class
var Obstacle = Container.expand(function () {
var self = Container.call(this);
// Randomly choose obstacle type: "low" (jump over), "high" (slide under), or "spike" (new)
var r = Math.random();
if (r < 0.45) {
self.type = 'low';
} else if (r < 0.9) {
self.type = 'high';
} else {
self.type = 'spike';
}
// Attach asset
if (self.type === 'low') {
// Low obstacle: box, short and wide
self.asset = self.attachAsset('obstacleLow', {
anchorX: 0.5,
anchorY: 1
});
} else if (self.type === 'high') {
// High obstacle: box, tall and narrow
self.asset = self.attachAsset('obstacleHigh', {
anchorX: 0.5,
anchorY: 1
});
} else {
// Spike obstacle: ellipse, short and narrow, dangerous
self.asset = self.attachAsset('obstacleSpike', {
anchorX: 0.5,
anchorY: 1
});
}
// Set size
if (self.type === 'low') {
self.asset.width = 180;
self.asset.height = 120;
} else if (self.type === 'high') {
self.asset.width = 120;
self.asset.height = 260;
} else {
self.asset.width = 120;
self.asset.height = 120;
}
// Update per frame
self.update = function () {
self.x -= gameSpeed;
};
return self;
});
// Player character class
var Runner = Container.expand(function () {
var self = Container.call(this);
// Attach runner asset (box for now)
var runnerAsset = self.attachAsset('runner', {
anchorX: 0.5,
anchorY: 1
});
// Physics
self.vy = 0;
self.isJumping = false;
self.isSliding = false;
self.slideTimer = 0;
// Constants
var GRAVITY = 2.2;
var JUMP_VELOCITY = -48;
var SLIDE_DURATION = 36; // ~0.6s at 60fps
// Runner size for collision
self.normalHeight = runnerAsset.height;
self.slideHeight = runnerAsset.height * 0.5;
// Methods
self.jump = function () {
if (!self.isJumping && !self.isSliding) {
self.vy = JUMP_VELOCITY;
self.isJumping = true;
}
};
self.slide = function () {
if (!self.isSliding && !self.isJumping) {
self.isSliding = true;
self.slideTimer = SLIDE_DURATION;
// Animate to slide
tween(runnerAsset, {
height: self.slideHeight
}, {
duration: 120,
easing: tween.cubicOut
});
}
};
self.standUp = function () {
if (self.isSliding) {
self.isSliding = false;
// Animate back to normal
tween(runnerAsset, {
height: self.normalHeight
}, {
duration: 120,
easing: tween.cubicOut
});
}
};
// Update per frame
self.update = function () {
// Jumping physics
if (self.isJumping) {
self.y += self.vy;
self.vy += GRAVITY;
if (self.y >= groundY) {
self.y = groundY;
self.vy = 0;
self.isJumping = false;
}
}
// Sliding timer
if (self.isSliding) {
self.slideTimer--;
if (self.slideTimer <= 0) {
self.standUp();
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222a36
});
/****
* Game Code
****/
// Game constants
// Background asset: a tall, wide box with a soft blue color for sky
var groundY = 2200; // Y position of the ground
var runnerStartX = 420;
var gameSpeed = 14.4; // Initial speed (pixels per frame) - reduced by 40%
var speedIncreaseEvery = 840; // Increase speed every N ticks - slowed by 40%
var speedIncreaseAmount = 0.72; // Speed increase per step - reduced by 40%
var maxGameSpeed = 28.8; // Max speed - reduced by 40%
// Arrays for obstacles and coins
var obstacles = [];
var coins = [];
// Score and distance
var score = 0;
var distance = 0;
// Obstacle spacing multiplier, increases by 10% every 1000m
var obstacleSpacingMultiplier = 1;
var lastDistanceMilestone = 0;
// Runner lives (can) system
var runnerLives = 5;
var maxRunnerLives = 5;
var heartIcons = [];
// Invulnerability state and timer
var isInvulnerable = false;
var invulnerableTimer = 0;
var INVULNERABLE_DURATION = 300; // 5 seconds at 60fps
for (var h = 0; h < maxRunnerLives; h++) {
var heart = new Text2("❤", {
size: 90,
fill: 0xFF4D4D
});
heart.anchor.set(0, 0);
LK.gui.top.addChild(heart);
heart.x = 240 + h * 80;
heart.y = 10;
heartIcons.push(heart);
}
function updateHearts() {
for (var h = 0; h < maxRunnerLives; h++) {
heartIcons[h].alpha = h < runnerLives ? 1 : 0.25;
}
}
updateHearts();
// Coin system
var coinsCollected = 0;
var coinTxt = new Text2('0', {
size: 90,
fill: 0xFFE066
});
coinTxt.anchor.set(0, 0);
LK.gui.top.addChild(coinTxt);
coinTxt.x = 120; // Avoid top left menu
coinTxt.y = 10;
// GUI
var scoreTxt = new Text2('0', {
size: 110,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var distanceTxt = new Text2('0m', {
size: 70,
fill: "#fff"
});
distanceTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(distanceTxt);
distanceTxt.y = 120;
// Add background asset (sky) behind all gameplay elements
var background = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChild(background);
// Create runner
var runner = new Runner();
game.addChild(runner);
runner.x = runnerStartX;
runner.y = groundY;
// Ground visual (simple box)
var ground = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 40,
y: groundY + 1,
x: 0
});
game.addChild(ground);
// Spawn timers
var obstacleTimer = 0;
var coinTimer = 0;
// Touch/gesture handling
var dragStartY = null;
var dragStartX = null;
var dragStartTime = null;
var gestureActive = false;
// Helper: check collision between two containers (AABB)
function isColliding(a, b) {
var ab = a.getBounds();
var bb = b.getBounds();
return ab.x + ab.width > bb.x && ab.x < bb.x + bb.width && ab.y + ab.height > bb.y && ab.y < bb.y + bb.height;
}
// Handle tap/swipe for jump/slide
game.down = function (x, y, obj) {
dragStartY = y;
dragStartX = x;
dragStartTime = LK.ticks;
gestureActive = true;
};
game.up = function (x, y, obj) {
if (!gestureActive) return;
var dy = y - dragStartY;
var dx = x - dragStartX;
var dt = LK.ticks - dragStartTime;
gestureActive = false;
// Simple swipe detection
if (dt < 30) {
// Quick gesture
if (Math.abs(dy) > Math.abs(dx)) {
if (dy < -80) {
// Swipe up: jump
runner.jump();
} else if (dy > 80) {
// Swipe down: slide
runner.slide();
}
} else {
// Could add left/right actions here
}
} else {
// Tap (short press): jump
if (Math.abs(dy) < 40 && Math.abs(dx) < 40) {
runner.jump();
}
}
};
// Prevent runner from being dragged
game.move = function (x, y, obj) {
// No drag for runner
};
// Main update loop
game.update = function () {
// Increase speed over time
if (LK.ticks % speedIncreaseEvery === 0 && gameSpeed < maxGameSpeed) {
gameSpeed += speedIncreaseAmount;
if (gameSpeed > maxGameSpeed) gameSpeed = maxGameSpeed;
}
// Update invulnerability timer and visual feedback
if (isInvulnerable) {
invulnerableTimer--;
// Flicker runner for feedback
var runnerAsset = runner.children[0];
runnerAsset.alpha = LK.ticks % 12 < 6 ? 0.4 : 1;
if (invulnerableTimer <= 0) {
isInvulnerable = false;
runnerAsset.alpha = 1;
}
}
// Update runner
runner.update();
// Spawn obstacles
obstacleTimer--;
if (obstacleTimer <= 0) {
var obs = new Obstacle();
obs.x = 2048 + 120;
// Every 1500 steps, drop obstacle from above runner
if (Math.floor(distance) > 0 && Math.floor(distance) % 1500 < gameSpeed * 0.18) {
// Place above runner, falling down
obs.x = runner.x + 40; // slightly ahead of runner
obs.y = groundY - 900; // start high above
obs.fallingOnRunner = true;
obs.fallVy = 0;
obs.fallGravity = 3.2 + Math.random() * 1.5;
} else {
if (obs.type === 'low') {
obs.y = groundY;
} else if (obs.type === 'high') {
// High obstacle: place so runner must slide under, 2% higher (10% - 8% = 2%)
obs.y = groundY - 100 - 0.02 * 2200; // 2200 is groundY, so 2% higher
} else {
// Spike obstacle: always at ground level
obs.y = groundY;
}
}
game.addChild(obs);
obstacles.push(obs);
// Next obstacle in 1.5-2.2s (randomized) - obstacles are a bit rarer
if (obs.type === 'low') {
// Increase low obstacle spacing by 15% and by 10% every 1000m
obstacleTimer = Math.floor((90 + Math.floor(Math.random() * 42)) * 0.85 * 0.8 * 0.9 * 0.85 * 1.15 * obstacleSpacingMultiplier);
} else if (obs.type === 'high') {
obstacleTimer = Math.floor((90 + Math.floor(Math.random() * 42)) * 0.8 * 0.9 * 0.85 * 1.15 * obstacleSpacingMultiplier);
} else {
// Spikes: slightly rarer, also increase by 15% more and by 10% every 1000m
obstacleTimer = Math.floor((120 + Math.floor(Math.random() * 60)) * 0.8 * 0.9 * 0.85 * 1.15 * obstacleSpacingMultiplier);
}
}
// Spawn coins
coinTimer--;
if (coinTimer <= 0) {
var coin = new Coin();
coin.x = 2048 + 90;
// Place coin at random height: above, at, or below runner
var coinYType = Math.floor(Math.random() * 3);
if (coinYType === 0) {
coin.y = groundY - 320; // high
} else if (coinYType === 1) {
coin.y = groundY - 120; // mid
} else {
coin.y = groundY - 40; // low
}
game.addChild(coin);
coins.push(coin);
// Next coin in 0.7-1.2s
coinTimer = 42 + Math.floor(Math.random() * 30);
}
// Update obstacles
for (var i = obstacles.length - 1; i >= 0; i--) {
var obs = obstacles[i];
// Update falling obstacles
if (obs.fallingOnRunner) {
if (obs.lastY === undefined) obs.lastY = obs.y;
obs.fallVy += obs.fallGravity;
obs.y += obs.fallVy;
// When it lands on ground, stop falling
if (obs.lastY < groundY && obs.y >= groundY) {
obs.y = groundY;
obs.fallingOnRunner = false;
obs.fallVy = 0;
}
obs.lastY = obs.y;
} else {
obs.update();
}
// Remove if off screen
if (obs.x < -200 || obs.y > 3000) {
obs.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with runner
if (isColliding(runner, obs)) {
// If invulnerable, ignore obstacle and do not remove it
if (isInvulnerable) {
continue;
}
// If obstacle is low, must jump; if high, must slide; if spike, must jump
if (obs.type === 'low') {
if (!runner.isJumping) {
LK.effects.flashScreen(0xff0000, 800);
runnerLives--;
updateHearts();
if (runnerLives <= 0) {
LK.showGameOver();
return;
}
// Set invulnerability
isInvulnerable = true;
invulnerableTimer = INVULNERABLE_DURATION;
// Remove obstacle and continue
obs.destroy();
obstacles.splice(i, 1);
continue;
}
} else if (obs.type === 'high') {
// High obstacle
var runnerAsset = runner.children[0];
if (!runner.isSliding || runnerAsset.height > runner.slideHeight + 2) {
LK.effects.flashScreen(0xff0000, 800);
runnerLives--;
updateHearts();
if (runnerLives <= 0) {
LK.showGameOver();
return;
}
// Set invulnerability
isInvulnerable = true;
invulnerableTimer = INVULNERABLE_DURATION;
// Remove obstacle and continue
obs.destroy();
obstacles.splice(i, 1);
continue;
}
} else if (obs.type === 'spike') {
// Spike: always hurts unless jumping
if (!runner.isJumping) {
LK.effects.flashScreen(0xff0000, 800);
runnerLives--;
updateHearts();
if (runnerLives <= 0) {
LK.showGameOver();
return;
}
// Set invulnerability
isInvulnerable = true;
invulnerableTimer = INVULNERABLE_DURATION;
// Remove obstacle and continue
obs.destroy();
obstacles.splice(i, 1);
continue;
}
}
}
}
// Update coins
for (var j = coins.length - 1; j >= 0; j--) {
var coin = coins[j];
coin.update();
// Remove if off screen
if (coin.x < -100) {
coin.destroy();
coins.splice(j, 1);
continue;
}
// Collect coin or heart
if (isColliding(runner, coin)) {
if (coin.isHeart) {
// Heart collectible: increase life if not max
if (runnerLives < maxRunnerLives) {
runnerLives++;
updateHearts();
LK.effects.flashObject(runner, 0xFF4D4D, 400);
}
} else {
score += 1;
LK.setScore(score);
scoreTxt.setText(score);
// Coin system: increment and update coin count
coinsCollected += 1;
coinTxt.setText(coinsCollected);
LK.effects.flashObject(runner, 0xffe066, 200);
}
coin.destroy();
coins.splice(j, 1);
}
}
// Update distance
distance += gameSpeed * 0.18; // scale to meters
distanceTxt.setText(Math.floor(distance) + "m");
// Every 1800 meters, increase obstacle spacing multiplier by 10% and game speed by 3%
if (Math.floor(distance / 1800) > lastDistanceMilestone) {
lastDistanceMilestone = Math.floor(distance / 1800);
obstacleSpacingMultiplier *= 1.1;
// Increase game speed by 3% every 1800 meters, but do not exceed maxGameSpeed
gameSpeed *= 1.03;
if (gameSpeed > maxGameSpeed) gameSpeed = maxGameSpeed;
// Add a heart collectible (life) every 1800 meters
if (runnerLives < maxRunnerLives) {
// Automatically restore one life
runnerLives++;
updateHearts();
LK.effects.flashObject(runner, 0xFF4D4D, 400);
// Still spawn a heart collectible for extra chance if not at max
var heartCollectible = new Coin(); // Use Coin class for heart collectible for now
heartCollectible.x = 2048 + 90;
heartCollectible.y = groundY - 320; // Place high to reward skill
// Change appearance to heart
if (heartCollectible.children.length > 0) {
var asset = heartCollectible.children[0];
asset.width = 90;
asset.height = 90;
asset.setText && asset.setText("❤");
asset.fill = 0xFF4D4D;
}
heartCollectible.isHeart = true;
game.addChild(heartCollectible);
coins.push(heartCollectible);
} else {
// If lives are full, spawn an extra heart collectible for bonus
var extraHeart = new Coin();
extraHeart.x = 2048 + 90;
extraHeart.y = groundY - 320;
if (extraHeart.children.length > 0) {
var asset = extraHeart.children[0];
asset.width = 90;
asset.height = 90;
asset.setText && asset.setText("❤");
asset.fill = 0xFF4D4D;
}
extraHeart.isHeart = true;
extraHeart.isBonusHeart = true; // Mark as bonus for possible future logic
game.addChild(extraHeart);
coins.push(extraHeart);
}
}
};
// Asset initialization (shapes) /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Coin class
var Coin = Container.expand(function () {
var self = Container.call(this);
// Attach asset (ellipse for now)
var coinAsset = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5
});
coinAsset.width = 90;
coinAsset.height = 90;
// Animation: simple up-down bob
self.bobPhase = Math.random() * Math.PI * 2;
self.update = function () {
self.x -= gameSpeed;
coinAsset.y = Math.sin(LK.ticks * 0.15 + self.bobPhase) * 12;
};
return self;
});
// Obstacle class
var Obstacle = Container.expand(function () {
var self = Container.call(this);
// Randomly choose obstacle type: "low" (jump over), "high" (slide under), or "spike" (new)
var r = Math.random();
if (r < 0.45) {
self.type = 'low';
} else if (r < 0.9) {
self.type = 'high';
} else {
self.type = 'spike';
}
// Attach asset
if (self.type === 'low') {
// Low obstacle: box, short and wide
self.asset = self.attachAsset('obstacleLow', {
anchorX: 0.5,
anchorY: 1
});
} else if (self.type === 'high') {
// High obstacle: box, tall and narrow
self.asset = self.attachAsset('obstacleHigh', {
anchorX: 0.5,
anchorY: 1
});
} else {
// Spike obstacle: ellipse, short and narrow, dangerous
self.asset = self.attachAsset('obstacleSpike', {
anchorX: 0.5,
anchorY: 1
});
}
// Set size
if (self.type === 'low') {
self.asset.width = 180;
self.asset.height = 120;
} else if (self.type === 'high') {
self.asset.width = 120;
self.asset.height = 260;
} else {
self.asset.width = 120;
self.asset.height = 120;
}
// Update per frame
self.update = function () {
self.x -= gameSpeed;
};
return self;
});
// Player character class
var Runner = Container.expand(function () {
var self = Container.call(this);
// Attach runner asset (box for now)
var runnerAsset = self.attachAsset('runner', {
anchorX: 0.5,
anchorY: 1
});
// Physics
self.vy = 0;
self.isJumping = false;
self.isSliding = false;
self.slideTimer = 0;
// Constants
var GRAVITY = 2.2;
var JUMP_VELOCITY = -48;
var SLIDE_DURATION = 36; // ~0.6s at 60fps
// Runner size for collision
self.normalHeight = runnerAsset.height;
self.slideHeight = runnerAsset.height * 0.5;
// Methods
self.jump = function () {
if (!self.isJumping && !self.isSliding) {
self.vy = JUMP_VELOCITY;
self.isJumping = true;
}
};
self.slide = function () {
if (!self.isSliding && !self.isJumping) {
self.isSliding = true;
self.slideTimer = SLIDE_DURATION;
// Animate to slide
tween(runnerAsset, {
height: self.slideHeight
}, {
duration: 120,
easing: tween.cubicOut
});
}
};
self.standUp = function () {
if (self.isSliding) {
self.isSliding = false;
// Animate back to normal
tween(runnerAsset, {
height: self.normalHeight
}, {
duration: 120,
easing: tween.cubicOut
});
}
};
// Update per frame
self.update = function () {
// Jumping physics
if (self.isJumping) {
self.y += self.vy;
self.vy += GRAVITY;
if (self.y >= groundY) {
self.y = groundY;
self.vy = 0;
self.isJumping = false;
}
}
// Sliding timer
if (self.isSliding) {
self.slideTimer--;
if (self.slideTimer <= 0) {
self.standUp();
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222a36
});
/****
* Game Code
****/
// Game constants
// Background asset: a tall, wide box with a soft blue color for sky
var groundY = 2200; // Y position of the ground
var runnerStartX = 420;
var gameSpeed = 14.4; // Initial speed (pixels per frame) - reduced by 40%
var speedIncreaseEvery = 840; // Increase speed every N ticks - slowed by 40%
var speedIncreaseAmount = 0.72; // Speed increase per step - reduced by 40%
var maxGameSpeed = 28.8; // Max speed - reduced by 40%
// Arrays for obstacles and coins
var obstacles = [];
var coins = [];
// Score and distance
var score = 0;
var distance = 0;
// Obstacle spacing multiplier, increases by 10% every 1000m
var obstacleSpacingMultiplier = 1;
var lastDistanceMilestone = 0;
// Runner lives (can) system
var runnerLives = 5;
var maxRunnerLives = 5;
var heartIcons = [];
// Invulnerability state and timer
var isInvulnerable = false;
var invulnerableTimer = 0;
var INVULNERABLE_DURATION = 300; // 5 seconds at 60fps
for (var h = 0; h < maxRunnerLives; h++) {
var heart = new Text2("❤", {
size: 90,
fill: 0xFF4D4D
});
heart.anchor.set(0, 0);
LK.gui.top.addChild(heart);
heart.x = 240 + h * 80;
heart.y = 10;
heartIcons.push(heart);
}
function updateHearts() {
for (var h = 0; h < maxRunnerLives; h++) {
heartIcons[h].alpha = h < runnerLives ? 1 : 0.25;
}
}
updateHearts();
// Coin system
var coinsCollected = 0;
var coinTxt = new Text2('0', {
size: 90,
fill: 0xFFE066
});
coinTxt.anchor.set(0, 0);
LK.gui.top.addChild(coinTxt);
coinTxt.x = 120; // Avoid top left menu
coinTxt.y = 10;
// GUI
var scoreTxt = new Text2('0', {
size: 110,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var distanceTxt = new Text2('0m', {
size: 70,
fill: "#fff"
});
distanceTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(distanceTxt);
distanceTxt.y = 120;
// Add background asset (sky) behind all gameplay elements
var background = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChild(background);
// Create runner
var runner = new Runner();
game.addChild(runner);
runner.x = runnerStartX;
runner.y = groundY;
// Ground visual (simple box)
var ground = LK.getAsset('ground', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 40,
y: groundY + 1,
x: 0
});
game.addChild(ground);
// Spawn timers
var obstacleTimer = 0;
var coinTimer = 0;
// Touch/gesture handling
var dragStartY = null;
var dragStartX = null;
var dragStartTime = null;
var gestureActive = false;
// Helper: check collision between two containers (AABB)
function isColliding(a, b) {
var ab = a.getBounds();
var bb = b.getBounds();
return ab.x + ab.width > bb.x && ab.x < bb.x + bb.width && ab.y + ab.height > bb.y && ab.y < bb.y + bb.height;
}
// Handle tap/swipe for jump/slide
game.down = function (x, y, obj) {
dragStartY = y;
dragStartX = x;
dragStartTime = LK.ticks;
gestureActive = true;
};
game.up = function (x, y, obj) {
if (!gestureActive) return;
var dy = y - dragStartY;
var dx = x - dragStartX;
var dt = LK.ticks - dragStartTime;
gestureActive = false;
// Simple swipe detection
if (dt < 30) {
// Quick gesture
if (Math.abs(dy) > Math.abs(dx)) {
if (dy < -80) {
// Swipe up: jump
runner.jump();
} else if (dy > 80) {
// Swipe down: slide
runner.slide();
}
} else {
// Could add left/right actions here
}
} else {
// Tap (short press): jump
if (Math.abs(dy) < 40 && Math.abs(dx) < 40) {
runner.jump();
}
}
};
// Prevent runner from being dragged
game.move = function (x, y, obj) {
// No drag for runner
};
// Main update loop
game.update = function () {
// Increase speed over time
if (LK.ticks % speedIncreaseEvery === 0 && gameSpeed < maxGameSpeed) {
gameSpeed += speedIncreaseAmount;
if (gameSpeed > maxGameSpeed) gameSpeed = maxGameSpeed;
}
// Update invulnerability timer and visual feedback
if (isInvulnerable) {
invulnerableTimer--;
// Flicker runner for feedback
var runnerAsset = runner.children[0];
runnerAsset.alpha = LK.ticks % 12 < 6 ? 0.4 : 1;
if (invulnerableTimer <= 0) {
isInvulnerable = false;
runnerAsset.alpha = 1;
}
}
// Update runner
runner.update();
// Spawn obstacles
obstacleTimer--;
if (obstacleTimer <= 0) {
var obs = new Obstacle();
obs.x = 2048 + 120;
// Every 1500 steps, drop obstacle from above runner
if (Math.floor(distance) > 0 && Math.floor(distance) % 1500 < gameSpeed * 0.18) {
// Place above runner, falling down
obs.x = runner.x + 40; // slightly ahead of runner
obs.y = groundY - 900; // start high above
obs.fallingOnRunner = true;
obs.fallVy = 0;
obs.fallGravity = 3.2 + Math.random() * 1.5;
} else {
if (obs.type === 'low') {
obs.y = groundY;
} else if (obs.type === 'high') {
// High obstacle: place so runner must slide under, 2% higher (10% - 8% = 2%)
obs.y = groundY - 100 - 0.02 * 2200; // 2200 is groundY, so 2% higher
} else {
// Spike obstacle: always at ground level
obs.y = groundY;
}
}
game.addChild(obs);
obstacles.push(obs);
// Next obstacle in 1.5-2.2s (randomized) - obstacles are a bit rarer
if (obs.type === 'low') {
// Increase low obstacle spacing by 15% and by 10% every 1000m
obstacleTimer = Math.floor((90 + Math.floor(Math.random() * 42)) * 0.85 * 0.8 * 0.9 * 0.85 * 1.15 * obstacleSpacingMultiplier);
} else if (obs.type === 'high') {
obstacleTimer = Math.floor((90 + Math.floor(Math.random() * 42)) * 0.8 * 0.9 * 0.85 * 1.15 * obstacleSpacingMultiplier);
} else {
// Spikes: slightly rarer, also increase by 15% more and by 10% every 1000m
obstacleTimer = Math.floor((120 + Math.floor(Math.random() * 60)) * 0.8 * 0.9 * 0.85 * 1.15 * obstacleSpacingMultiplier);
}
}
// Spawn coins
coinTimer--;
if (coinTimer <= 0) {
var coin = new Coin();
coin.x = 2048 + 90;
// Place coin at random height: above, at, or below runner
var coinYType = Math.floor(Math.random() * 3);
if (coinYType === 0) {
coin.y = groundY - 320; // high
} else if (coinYType === 1) {
coin.y = groundY - 120; // mid
} else {
coin.y = groundY - 40; // low
}
game.addChild(coin);
coins.push(coin);
// Next coin in 0.7-1.2s
coinTimer = 42 + Math.floor(Math.random() * 30);
}
// Update obstacles
for (var i = obstacles.length - 1; i >= 0; i--) {
var obs = obstacles[i];
// Update falling obstacles
if (obs.fallingOnRunner) {
if (obs.lastY === undefined) obs.lastY = obs.y;
obs.fallVy += obs.fallGravity;
obs.y += obs.fallVy;
// When it lands on ground, stop falling
if (obs.lastY < groundY && obs.y >= groundY) {
obs.y = groundY;
obs.fallingOnRunner = false;
obs.fallVy = 0;
}
obs.lastY = obs.y;
} else {
obs.update();
}
// Remove if off screen
if (obs.x < -200 || obs.y > 3000) {
obs.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with runner
if (isColliding(runner, obs)) {
// If invulnerable, ignore obstacle and do not remove it
if (isInvulnerable) {
continue;
}
// If obstacle is low, must jump; if high, must slide; if spike, must jump
if (obs.type === 'low') {
if (!runner.isJumping) {
LK.effects.flashScreen(0xff0000, 800);
runnerLives--;
updateHearts();
if (runnerLives <= 0) {
LK.showGameOver();
return;
}
// Set invulnerability
isInvulnerable = true;
invulnerableTimer = INVULNERABLE_DURATION;
// Remove obstacle and continue
obs.destroy();
obstacles.splice(i, 1);
continue;
}
} else if (obs.type === 'high') {
// High obstacle
var runnerAsset = runner.children[0];
if (!runner.isSliding || runnerAsset.height > runner.slideHeight + 2) {
LK.effects.flashScreen(0xff0000, 800);
runnerLives--;
updateHearts();
if (runnerLives <= 0) {
LK.showGameOver();
return;
}
// Set invulnerability
isInvulnerable = true;
invulnerableTimer = INVULNERABLE_DURATION;
// Remove obstacle and continue
obs.destroy();
obstacles.splice(i, 1);
continue;
}
} else if (obs.type === 'spike') {
// Spike: always hurts unless jumping
if (!runner.isJumping) {
LK.effects.flashScreen(0xff0000, 800);
runnerLives--;
updateHearts();
if (runnerLives <= 0) {
LK.showGameOver();
return;
}
// Set invulnerability
isInvulnerable = true;
invulnerableTimer = INVULNERABLE_DURATION;
// Remove obstacle and continue
obs.destroy();
obstacles.splice(i, 1);
continue;
}
}
}
}
// Update coins
for (var j = coins.length - 1; j >= 0; j--) {
var coin = coins[j];
coin.update();
// Remove if off screen
if (coin.x < -100) {
coin.destroy();
coins.splice(j, 1);
continue;
}
// Collect coin or heart
if (isColliding(runner, coin)) {
if (coin.isHeart) {
// Heart collectible: increase life if not max
if (runnerLives < maxRunnerLives) {
runnerLives++;
updateHearts();
LK.effects.flashObject(runner, 0xFF4D4D, 400);
}
} else {
score += 1;
LK.setScore(score);
scoreTxt.setText(score);
// Coin system: increment and update coin count
coinsCollected += 1;
coinTxt.setText(coinsCollected);
LK.effects.flashObject(runner, 0xffe066, 200);
}
coin.destroy();
coins.splice(j, 1);
}
}
// Update distance
distance += gameSpeed * 0.18; // scale to meters
distanceTxt.setText(Math.floor(distance) + "m");
// Every 1800 meters, increase obstacle spacing multiplier by 10% and game speed by 3%
if (Math.floor(distance / 1800) > lastDistanceMilestone) {
lastDistanceMilestone = Math.floor(distance / 1800);
obstacleSpacingMultiplier *= 1.1;
// Increase game speed by 3% every 1800 meters, but do not exceed maxGameSpeed
gameSpeed *= 1.03;
if (gameSpeed > maxGameSpeed) gameSpeed = maxGameSpeed;
// Add a heart collectible (life) every 1800 meters
if (runnerLives < maxRunnerLives) {
// Automatically restore one life
runnerLives++;
updateHearts();
LK.effects.flashObject(runner, 0xFF4D4D, 400);
// Still spawn a heart collectible for extra chance if not at max
var heartCollectible = new Coin(); // Use Coin class for heart collectible for now
heartCollectible.x = 2048 + 90;
heartCollectible.y = groundY - 320; // Place high to reward skill
// Change appearance to heart
if (heartCollectible.children.length > 0) {
var asset = heartCollectible.children[0];
asset.width = 90;
asset.height = 90;
asset.setText && asset.setText("❤");
asset.fill = 0xFF4D4D;
}
heartCollectible.isHeart = true;
game.addChild(heartCollectible);
coins.push(heartCollectible);
} else {
// If lives are full, spawn an extra heart collectible for bonus
var extraHeart = new Coin();
extraHeart.x = 2048 + 90;
extraHeart.y = groundY - 320;
if (extraHeart.children.length > 0) {
var asset = extraHeart.children[0];
asset.width = 90;
asset.height = 90;
asset.setText && asset.setText("❤");
asset.fill = 0xFF4D4D;
}
extraHeart.isHeart = true;
extraHeart.isBonusHeart = true; // Mark as bonus for possible future logic
game.addChild(extraHeart);
coins.push(extraHeart);
}
}
};
// Asset initialization (shapes)
Street. In-Game asset. 2d. High contrast. No shadows
Sprey. In-Game asset. 2d. High contrast. No shadows
Street wall. In-Game asset. 2d. High contrast. No shadows
Duvar. In-Game asset. 2d. High contrast. No shadows
Gercekci duvar. In-Game asset. 2d. High contrast. No shadows
Kosucu. In-Game asset. 2d. High contrast. No shadows