/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Coin class
var Coin = Container.expand(function () {
var self = Container.call(this);
var coinAsset = self.attachAsset('coinCircle', {
anchorX: 0.5,
anchorY: 0.5
});
self.lane = 1;
self.speed = 0;
self.update = function () {
self.y += self.speed;
};
return self;
});
// Obstacle class (trains, barriers)
var Obstacle = Container.expand(function () {
var self = Container.call(this);
// Attach asset for single obstacle
var obsSingle1 = self.attachAsset('obstacleBox', {
anchorX: 0.5,
anchorY: 0.5,
visible: true
});
// Attach asset for train obstacle
var obsTrain1 = self.attachAsset('obstacleTrain', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
});
self.lane = 1;
self.type = 'barrier'; // or 'train'
self.speed = 0; // set by game
// Animation state for obstacle
self.animFrame = 0;
self.animInterval = 18; // slightly slower than runner
self.animTick = 0;
self.setType = function (type) {
self.type = type;
// Set asset visibility
if (type === 'barrier') {
obsSingle1.visible = true;
obsTrain1.visible = false;
// Set size for single
self.width = obsSingle1.width;
self.height = obsSingle1.height;
} else {
obsSingle1.visible = false;
obsTrain1.visible = true;
// Set size for train
self.width = obsTrain1.width;
self.height = obsTrain1.height;
}
self.animFrame = 0;
self.animTick = 0;
};
self.update = function () {
self.y += self.speed;
// No animation for obstacles (only one asset per type)
};
return self;
});
// Powerup class
var Powerup = Container.expand(function () {
var self = Container.call(this);
var powerupAsset = self.attachAsset('powerupStar', {
anchorX: 0.5,
anchorY: 0.5
});
self.lane = 1;
self.speed = 0;
self.kind = 'invincible'; // or 'magnet'
self.update = function () {
self.y += self.speed;
};
return self;
});
// Player character class
var Runner = Container.expand(function () {
var self = Container.call(this);
// Attach two runner assets for animation
var runnerAsset1 = self.attachAsset('runnerBox', {
anchorX: 0.5,
anchorY: 0.5
});
var runnerAsset2 = self.attachAsset('runnerBox2', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
});
// Lane index: 0 (left), 1 (center), 2 (right)
self.lane = 1;
self.y = 0; // Will be set by game
self.isJumping = false;
self.jumpStartY = 0;
self.jumpTime = 0;
self.jumpDuration = 44; // frames (longer jump, about 0.73s)
self.jumpHeight = 420; // px (higher jump)
// Animation state
self.animFrame = 0;
self.animInterval = 16; // frames between switching (slower animation)
self.animTick = 0;
// Move to lane (with tween)
self.moveToLane = function (laneIdx) {
self.lane = laneIdx;
var targetX = laneX(laneIdx);
tween(self, {
x: targetX
}, {
duration: 120,
easing: tween.cubicOut
});
};
// Start jump
self.jump = function () {
if (self.isJumping) return;
self.isJumping = true;
self.jumpStartY = self.y;
self.jumpTime = 0;
};
// Update per frame
self.update = function () {
// Animation: alternate visible asset every animInterval frames
self.animTick++;
if (self.animTick >= self.animInterval) {
self.animTick = 0;
self.animFrame = 1 - self.animFrame;
runnerAsset1.visible = self.animFrame === 0;
runnerAsset2.visible = self.animFrame === 1;
}
if (self.isJumping) {
self.jumpTime++;
// Simple parabolic jump
var t = self.jumpTime / self.jumpDuration;
if (t > 1) t = 1;
var jumpOffset = -4 * self.jumpHeight * t * (t - 1);
self.y = runnerY() + jumpOffset;
// Squash and stretch effect
var scaleY, scaleX;
if (t < 0.15) {
// Squash at takeoff
scaleY = 0.7 + 0.3 * (t / 0.15);
scaleX = 1.2 - 0.2 * (t / 0.15);
} else if (t > 0.85) {
// Squash at landing
var t2 = (t - 0.85) / 0.15;
scaleY = 1.0 - 0.3 * t2;
scaleX = 1.0 + 0.2 * t2;
} else {
// Stretch in air
scaleY = 1.15 - 0.15 * ((t - 0.15) / 0.7);
scaleX = 0.85 + 0.15 * ((t - 0.15) / 0.7);
}
runnerAsset1.scale.y = scaleY;
runnerAsset1.scale.x = scaleX;
runnerAsset2.scale.y = scaleY;
runnerAsset2.scale.x = scaleX;
if (self.jumpTime >= self.jumpDuration) {
self.isJumping = false;
self.y = runnerY();
// Reset scale
runnerAsset1.scale.y = 1;
runnerAsset1.scale.x = 1;
runnerAsset2.scale.y = 1;
runnerAsset2.scale.x = 1;
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a6cff
});
/****
* Game Code
****/
// Rail images for 3 lanes (tiled vertically for better appearance)
// new single obstacle asset
// new train obstacle asset
// new train obstacle asset
var rails = [];
var railAssetNames = ['railLeft', 'railCenter', 'railRight'];
var railTileHeight = 400; // Use a reasonable tile height for the rail image
var railImageHeight = LK.getAsset(railAssetNames[0], {
anchorX: 0.5,
anchorY: 0
}).height;
var railImageWidth = LK.getAsset(railAssetNames[0], {
anchorX: 0.5,
anchorY: 0
}).width;
var railTilesCount = Math.ceil(2732 / railTileHeight) + 1;
for (var i = 0; i < 3; i++) {
rails[i] = [];
for (var t = 0; t < railTilesCount; t++) {
var rail = LK.getAsset(railAssetNames[i], {
anchorX: 0.5,
anchorY: 0,
x: laneX(i),
y: t * railTileHeight,
width: railImageWidth,
height: railTileHeight
});
rails[i].push(rail);
game.addChild(rail);
}
}
// --- Lane positions ---
function laneX(idx) {
// 3 lanes, centered, with 320px between centers
var center = 2048 / 2;
return center + (idx - 1) * 320;
}
function runnerY() {
// Place runner near bottom, but above bottom edge
return 2732 - 420;
}
// --- Game state ---
var runner;
var obstacles = [];
var coins = [];
var powerups = [];
var score = 0;
var coinScore = 0;
var speed = 10; // px per frame, much slower start, increases over time
var ticks = 0;
var invincibleTicks = 0;
var magnetTicks = 0;
var lastSwipeX = null;
var lastSwipeY = null;
var swipeStartX = null;
var swipeStartY = null;
var swipeStartTime = null;
var isGameOver = false;
// --- Score display ---
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Coin display ---
var coinTxt = new Text2('0', {
size: 80,
fill: 0xF7D038
});
// Set anchor to right/top and add margin from the edge
coinTxt.anchor.set(1, 0);
coinTxt.x = -40; // 40px margin from right edge
coinTxt.y = 0; // flush with top
LK.gui.topRight.addChild(coinTxt);
// --- Powerup display (bottom center, with timer below) ---
var powerupTxt = new Text2('', {
size: 60,
fill: 0x83DE44
});
powerupTxt.anchor.set(0.5, 0); // center horizontally, top edge
powerupTxt.x = 0;
powerupTxt.y = 0;
var powerupTimerTxt = new Text2('', {
size: 48,
fill: 0xffffff
});
powerupTimerTxt.anchor.set(0.5, 0); // center horizontally, top edge
powerupTimerTxt.x = 0;
powerupTimerTxt.y = 70; // 70px below the main text
// Create a container for both texts
var powerupContainer = new Container();
powerupContainer.addChild(powerupTxt);
powerupContainer.addChild(powerupTimerTxt);
// Position container at bottom center, above the very bottom (e.g. 120px up)
powerupContainer.x = 0;
powerupContainer.y = -120;
LK.gui.bottom.addChild(powerupContainer);
// --- MENU OVERLAY ---
var menuOverlay = new Container();
menuOverlay.interactive = true;
menuOverlay.visible = true;
// Decorative background (cover full screen)
var menuBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
scaleX: 30,
scaleY: 30,
tint: 0x1a6cff,
// blue background to match game
alpha: 0.92
});
menuOverlay.addChild(menuBg);
// (Removed decorative accent that appeared as a light green square in the center)
// (Removed menuTitleShadow and menuTitle for logo insertion)
// Add Subway Dashers logo image to menu overlay (top center)
var logoImg = LK.getAsset('subwayDashersLogo', {
anchorX: 0.5,
anchorY: 0,
x: 0,
y: -1200,
scaleX: 7.2,
scaleY: 7.2
});
menuOverlay.addChild(logoImg);
// (Subtitle removed)
// Play button with modern glassy background and shadow (move to bottom center of menu)
var playBtnBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 700,
scaleX: 5.2,
scaleY: 2.2,
tint: 0xffffff,
alpha: 0.18
});
menuOverlay.addChild(playBtnBg);
var playBtnShadow = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 710,
scaleX: 5.2,
scaleY: 2.2,
tint: 0x000000,
alpha: 0.13
});
menuOverlay.addChild(playBtnShadow);
var playBtnAccent = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 700,
scaleX: 4.7,
scaleY: 1.7,
// Use a more orange color
tint: 0xFF8C1A,
alpha: 0.97
});
menuOverlay.addChild(playBtnAccent);
var playBtn = new Text2('START', {
size: 120,
fill: "#fff",
font: "Impact, 'Arial Black', Tahoma",
alpha: 0.98,
shadow: {
color: "#000",
blur: 12,
x: 0,
y: 8
}
});
playBtn.anchor.set(0.5, 0.5);
playBtn.x = 0;
playBtn.y = 700;
menuOverlay.addChild(playBtn);
// Add a subtle divider line (move above play button)
var divider = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 520,
scaleX: 2.8,
scaleY: 0.09,
tint: 0xffffff,
alpha: 0.18
});
menuOverlay.addChild(divider);
// Add a subtle glassy highlight above the button for polish (move above play button)
var menuHighlight = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 540,
scaleX: 2.2,
scaleY: 0.22,
tint: 0xffffff,
alpha: 0.13
});
menuOverlay.addChild(menuHighlight);
// Decorative hint text, more modern (move further below play button)
var hintTxt = new Text2('Swipe left/right to move ยท Swipe up to jump', {
size: 54,
fill: "#fff",
alpha: 0.82
});
hintTxt.anchor.set(0.5, 0.5);
hintTxt.x = 0;
hintTxt.y = 950; // moved further down
menuOverlay.addChild(hintTxt);
// Add a soft drop shadow under the menu for depth
var menuDropShadow = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 420,
scaleX: 7.5,
scaleY: 1.1,
tint: 0x000000,
alpha: 0.10
});
menuOverlay.addChild(menuDropShadow);
// Center overlay horizontally and vertically for full screen coverage
menuOverlay.x = 2048 / 2;
menuOverlay.y = 2732 / 2;
// Add to stage
game.addChild(menuOverlay);
// --- Initialize runner ---
runner = new Runner();
runner.x = laneX(1);
runner.y = runnerY();
game.addChild(runner);
// Hide runner and UI until game starts
runner.visible = false;
scoreTxt.visible = false;
coinTxt.visible = false;
powerupContainer.visible = false;
// --- Touch/Swipe controls ---
var gameStarted = false;
// Block all input until Play is pressed
game.down = function (x, y, obj) {
if (!gameStarted) return;
swipeStartX = x;
swipeStartY = y;
swipeStartTime = LK.ticks;
lastSwipeX = x;
lastSwipeY = y;
};
game.move = function (x, y, obj) {
if (!gameStarted) return;
if (swipeStartX === null) return;
var dx = x - swipeStartX;
var dy = y - swipeStartY;
var absDx = Math.abs(dx);
var absDy = Math.abs(dy);
var swipeThreshold = 120;
var swipeTime = LK.ticks - swipeStartTime;
if (swipeTime > 36) {
// Too slow, reset
swipeStartX = null;
swipeStartY = null;
return;
}
if (absDx > absDy && absDx > swipeThreshold) {
// Horizontal swipe
if (dx > 0 && runner.lane < 2) {
runner.moveToLane(runner.lane + 1);
} else if (dx < 0 && runner.lane > 0) {
runner.moveToLane(runner.lane - 1);
}
swipeStartX = null;
swipeStartY = null;
} else if (absDy > absDx && dy < -swipeThreshold) {
// Upward swipe (jump)
runner.jump();
swipeStartX = null;
swipeStartY = null;
}
};
game.up = function (x, y, obj) {
if (!gameStarted) return;
swipeStartX = null;
swipeStartY = null;
};
function startGame() {
if (gameStarted) return;
gameStarted = true;
menuOverlay.visible = false;
runner.visible = true;
scoreTxt.visible = true;
coinTxt.visible = true;
powerupContainer.visible = true;
// Reset state
isGameOver = false;
score = 0;
coinScore = 0;
speed = 10;
ticks = 0;
invincibleTicks = 0;
magnetTicks = 0;
scoreTxt.setText('0');
coinTxt.setText('0');
powerupTxt.setText('');
powerupTimerTxt.setText('');
}
// Play button interaction
playBtn.interactive = true;
playBtn.down = function (x, y, obj) {
startGame();
};
// Remove tap-anywhere-to-start: only play button starts the game
menuOverlay.down = function (x, y, obj) {
// Do nothing, only playBtn starts the game
};
game.down = function (x, y, obj) {
if (!gameStarted) return;
swipeStartX = x;
swipeStartY = y;
swipeStartTime = LK.ticks;
lastSwipeX = x;
lastSwipeY = y;
};
game.move = function (x, y, obj) {
if (!gameStarted) return;
if (swipeStartX === null) return;
var dx = x - swipeStartX;
var dy = y - swipeStartY;
var absDx = Math.abs(dx);
var absDy = Math.abs(dy);
var swipeThreshold = 120;
var swipeTime = LK.ticks - swipeStartTime;
if (swipeTime > 36) {
// Too slow, reset
swipeStartX = null;
swipeStartY = null;
return;
}
if (absDx > absDy && absDx > swipeThreshold) {
// Horizontal swipe
if (dx > 0 && runner.lane < 2) {
runner.moveToLane(runner.lane + 1);
} else if (dx < 0 && runner.lane > 0) {
runner.moveToLane(runner.lane - 1);
}
swipeStartX = null;
swipeStartY = null;
} else if (absDy > absDx && dy < -swipeThreshold) {
// Upward swipe (jump)
runner.jump();
swipeStartX = null;
swipeStartY = null;
}
};
game.up = function (x, y, obj) {
if (!gameStarted) return;
swipeStartX = null;
swipeStartY = null;
};
// Show menu again on game over
game.on('destroy', function () {
menuOverlay.visible = true;
runner.visible = false;
scoreTxt.visible = false;
coinTxt.visible = false;
powerupContainer.visible = false;
gameStarted = false;
});
// --- Helper: spawn obstacle ---
function spawnObstacle() {
var obs = new Obstacle();
obs.lane = Math.floor(Math.random() * 3);
obs.x = laneX(obs.lane);
obs.y = -120;
var type = Math.random() < 0.7 ? 'barrier' : 'train';
obs.setType(type);
obs.speed = speed;
obstacles.push(obs);
game.addChild(obs);
}
// --- Helper: spawn coin ---
function spawnCoin() {
var coin = new Coin();
coin.lane = Math.floor(Math.random() * 3);
coin.x = laneX(coin.lane);
coin.y = -80;
coin.speed = speed;
coins.push(coin);
game.addChild(coin);
}
// --- Helper: spawn powerup ---
function spawnPowerup() {
var powerup = new Powerup();
powerup.lane = Math.floor(Math.random() * 3);
powerup.x = laneX(powerup.lane);
powerup.y = -100;
powerup.speed = speed;
powerup.kind = Math.random() < 0.5 ? 'invincible' : 'magnet';
powerups.push(powerup);
game.addChild(powerup);
}
// --- Main update loop ---
game.update = function () {
if (!gameStarted) return;
if (isGameOver) return;
ticks++;
// Increase speed over time
// Old: every 180 ticks, +1.2, max 38
// New: every 180 ticks, +0.6, max 38 (so it takes twice as long to reach max speed)
if (ticks % 180 === 0 && speed < 38) {
speed += 0.6;
}
// Update runner
runner.update();
// Update powerup timers
if (invincibleTicks > 0) {
invincibleTicks--;
powerupTxt.setText("INVINCIBLE " + Math.ceil(invincibleTicks / 60).toString() + "s");
powerupTimerTxt.setText("");
} else if (magnetTicks > 0) {
magnetTicks--;
powerupTxt.setText("MAGNET " + Math.ceil(magnetTicks / 60).toString() + "s");
powerupTimerTxt.setText("");
} else {
powerupTxt.setText("");
powerupTimerTxt.setText("");
}
// Spawn obstacles
// 25% more obstacles: reduce interval by 25%
if (ticks % Math.max(36, Math.floor((120 - speed * 2) * 0.75)) === 0) {
spawnObstacle();
}
// Spawn coins
if (ticks % 36 === 0) {
spawnCoin();
}
// Spawn powerups
if (ticks % 420 === 0) {
spawnPowerup();
}
// --- Update obstacles ---
for (var i = obstacles.length - 1; i >= 0; i--) {
var obs = obstacles[i];
obs.speed = speed;
obs.update();
// Remove if off screen
if (obs.y > 2732 + 200) {
obs.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with runner
var collides = false;
if (obs.lane === runner.lane) {
// If runner is jumping, only collide with trains (tall obstacles)
if (obs.type === 'train') {
// Trains are tall, can hit in air
if (Math.abs(obs.y - runner.y) < 180) {
collides = true;
}
} else {
// Barriers: only if runner is not jumping
if (!runner.isJumping && Math.abs(obs.y - runner.y) < 160) {
collides = true;
}
}
}
if (collides && invincibleTicks === 0) {
// Game over
LK.effects.flashScreen(0xff0000, 800);
isGameOver = true;
LK.setScore(score);
LK.showGameOver();
return;
}
}
// --- Update coins ---
for (var j = coins.length - 1; j >= 0; j--) {
var coin = coins[j];
coin.speed = speed;
coin.update();
// Remove if off screen
if (coin.y > 2732 + 100) {
coin.destroy();
coins.splice(j, 1);
continue;
}
// Collect coin
var collect = false;
if (magnetTicks > 0) {
// Magnet: collect if in any lane and close
if (Math.abs(coin.y - runner.y) < 220) {
collect = true;
}
} else {
if (coin.lane === runner.lane && Math.abs(coin.y - runner.y) < 120) {
collect = true;
}
}
if (collect) {
coinScore++;
score += 10;
coinTxt.setText(coinScore);
scoreTxt.setText(score);
coin.destroy();
coins.splice(j, 1);
}
}
// --- Update powerups ---
for (var k = powerups.length - 1; k >= 0; k--) {
var p = powerups[k];
p.speed = speed;
p.update();
// Remove if off screen
if (p.y > 2732 + 120) {
p.destroy();
powerups.splice(k, 1);
continue;
}
// Collect powerup
var collectP = false;
if (p.lane === runner.lane && Math.abs(p.y - runner.y) < 140) {
collectP = true;
}
if (collectP) {
if (p.kind === 'invincible') {
invincibleTicks = 180; // 3 seconds
LK.effects.flashObject(runner, 0x83de44, 800);
} else if (p.kind === 'magnet') {
magnetTicks = 240; // 4 seconds
LK.effects.flashObject(runner, 0xf7d038, 800);
}
p.destroy();
powerups.splice(k, 1);
}
}
// --- Score increases over time ---
if (ticks % 12 === 0) {
score++;
scoreTxt.setText(score);
}
};
// --- Reset state on game over ---
game.on('destroy', function () {
// Clean up arrays
for (var i = 0; i < obstacles.length; i++) obstacles[i].destroy();
for (var j = 0; j < coins.length; j++) coins[j].destroy();
for (var k = 0; k < powerups.length; k++) powerups[k].destroy();
obstacles = [];
coins = [];
powerups = [];
isGameOver = false;
score = 0;
coinScore = 0;
speed = 18;
ticks = 0;
invincibleTicks = 0;
magnetTicks = 0;
scoreTxt.setText('0');
coinTxt.setText('0');
powerupTxt.setText('');
}); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Coin class
var Coin = Container.expand(function () {
var self = Container.call(this);
var coinAsset = self.attachAsset('coinCircle', {
anchorX: 0.5,
anchorY: 0.5
});
self.lane = 1;
self.speed = 0;
self.update = function () {
self.y += self.speed;
};
return self;
});
// Obstacle class (trains, barriers)
var Obstacle = Container.expand(function () {
var self = Container.call(this);
// Attach asset for single obstacle
var obsSingle1 = self.attachAsset('obstacleBox', {
anchorX: 0.5,
anchorY: 0.5,
visible: true
});
// Attach asset for train obstacle
var obsTrain1 = self.attachAsset('obstacleTrain', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
});
self.lane = 1;
self.type = 'barrier'; // or 'train'
self.speed = 0; // set by game
// Animation state for obstacle
self.animFrame = 0;
self.animInterval = 18; // slightly slower than runner
self.animTick = 0;
self.setType = function (type) {
self.type = type;
// Set asset visibility
if (type === 'barrier') {
obsSingle1.visible = true;
obsTrain1.visible = false;
// Set size for single
self.width = obsSingle1.width;
self.height = obsSingle1.height;
} else {
obsSingle1.visible = false;
obsTrain1.visible = true;
// Set size for train
self.width = obsTrain1.width;
self.height = obsTrain1.height;
}
self.animFrame = 0;
self.animTick = 0;
};
self.update = function () {
self.y += self.speed;
// No animation for obstacles (only one asset per type)
};
return self;
});
// Powerup class
var Powerup = Container.expand(function () {
var self = Container.call(this);
var powerupAsset = self.attachAsset('powerupStar', {
anchorX: 0.5,
anchorY: 0.5
});
self.lane = 1;
self.speed = 0;
self.kind = 'invincible'; // or 'magnet'
self.update = function () {
self.y += self.speed;
};
return self;
});
// Player character class
var Runner = Container.expand(function () {
var self = Container.call(this);
// Attach two runner assets for animation
var runnerAsset1 = self.attachAsset('runnerBox', {
anchorX: 0.5,
anchorY: 0.5
});
var runnerAsset2 = self.attachAsset('runnerBox2', {
anchorX: 0.5,
anchorY: 0.5,
visible: false
});
// Lane index: 0 (left), 1 (center), 2 (right)
self.lane = 1;
self.y = 0; // Will be set by game
self.isJumping = false;
self.jumpStartY = 0;
self.jumpTime = 0;
self.jumpDuration = 44; // frames (longer jump, about 0.73s)
self.jumpHeight = 420; // px (higher jump)
// Animation state
self.animFrame = 0;
self.animInterval = 16; // frames between switching (slower animation)
self.animTick = 0;
// Move to lane (with tween)
self.moveToLane = function (laneIdx) {
self.lane = laneIdx;
var targetX = laneX(laneIdx);
tween(self, {
x: targetX
}, {
duration: 120,
easing: tween.cubicOut
});
};
// Start jump
self.jump = function () {
if (self.isJumping) return;
self.isJumping = true;
self.jumpStartY = self.y;
self.jumpTime = 0;
};
// Update per frame
self.update = function () {
// Animation: alternate visible asset every animInterval frames
self.animTick++;
if (self.animTick >= self.animInterval) {
self.animTick = 0;
self.animFrame = 1 - self.animFrame;
runnerAsset1.visible = self.animFrame === 0;
runnerAsset2.visible = self.animFrame === 1;
}
if (self.isJumping) {
self.jumpTime++;
// Simple parabolic jump
var t = self.jumpTime / self.jumpDuration;
if (t > 1) t = 1;
var jumpOffset = -4 * self.jumpHeight * t * (t - 1);
self.y = runnerY() + jumpOffset;
// Squash and stretch effect
var scaleY, scaleX;
if (t < 0.15) {
// Squash at takeoff
scaleY = 0.7 + 0.3 * (t / 0.15);
scaleX = 1.2 - 0.2 * (t / 0.15);
} else if (t > 0.85) {
// Squash at landing
var t2 = (t - 0.85) / 0.15;
scaleY = 1.0 - 0.3 * t2;
scaleX = 1.0 + 0.2 * t2;
} else {
// Stretch in air
scaleY = 1.15 - 0.15 * ((t - 0.15) / 0.7);
scaleX = 0.85 + 0.15 * ((t - 0.15) / 0.7);
}
runnerAsset1.scale.y = scaleY;
runnerAsset1.scale.x = scaleX;
runnerAsset2.scale.y = scaleY;
runnerAsset2.scale.x = scaleX;
if (self.jumpTime >= self.jumpDuration) {
self.isJumping = false;
self.y = runnerY();
// Reset scale
runnerAsset1.scale.y = 1;
runnerAsset1.scale.x = 1;
runnerAsset2.scale.y = 1;
runnerAsset2.scale.x = 1;
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a6cff
});
/****
* Game Code
****/
// Rail images for 3 lanes (tiled vertically for better appearance)
// new single obstacle asset
// new train obstacle asset
// new train obstacle asset
var rails = [];
var railAssetNames = ['railLeft', 'railCenter', 'railRight'];
var railTileHeight = 400; // Use a reasonable tile height for the rail image
var railImageHeight = LK.getAsset(railAssetNames[0], {
anchorX: 0.5,
anchorY: 0
}).height;
var railImageWidth = LK.getAsset(railAssetNames[0], {
anchorX: 0.5,
anchorY: 0
}).width;
var railTilesCount = Math.ceil(2732 / railTileHeight) + 1;
for (var i = 0; i < 3; i++) {
rails[i] = [];
for (var t = 0; t < railTilesCount; t++) {
var rail = LK.getAsset(railAssetNames[i], {
anchorX: 0.5,
anchorY: 0,
x: laneX(i),
y: t * railTileHeight,
width: railImageWidth,
height: railTileHeight
});
rails[i].push(rail);
game.addChild(rail);
}
}
// --- Lane positions ---
function laneX(idx) {
// 3 lanes, centered, with 320px between centers
var center = 2048 / 2;
return center + (idx - 1) * 320;
}
function runnerY() {
// Place runner near bottom, but above bottom edge
return 2732 - 420;
}
// --- Game state ---
var runner;
var obstacles = [];
var coins = [];
var powerups = [];
var score = 0;
var coinScore = 0;
var speed = 10; // px per frame, much slower start, increases over time
var ticks = 0;
var invincibleTicks = 0;
var magnetTicks = 0;
var lastSwipeX = null;
var lastSwipeY = null;
var swipeStartX = null;
var swipeStartY = null;
var swipeStartTime = null;
var isGameOver = false;
// --- Score display ---
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Coin display ---
var coinTxt = new Text2('0', {
size: 80,
fill: 0xF7D038
});
// Set anchor to right/top and add margin from the edge
coinTxt.anchor.set(1, 0);
coinTxt.x = -40; // 40px margin from right edge
coinTxt.y = 0; // flush with top
LK.gui.topRight.addChild(coinTxt);
// --- Powerup display (bottom center, with timer below) ---
var powerupTxt = new Text2('', {
size: 60,
fill: 0x83DE44
});
powerupTxt.anchor.set(0.5, 0); // center horizontally, top edge
powerupTxt.x = 0;
powerupTxt.y = 0;
var powerupTimerTxt = new Text2('', {
size: 48,
fill: 0xffffff
});
powerupTimerTxt.anchor.set(0.5, 0); // center horizontally, top edge
powerupTimerTxt.x = 0;
powerupTimerTxt.y = 70; // 70px below the main text
// Create a container for both texts
var powerupContainer = new Container();
powerupContainer.addChild(powerupTxt);
powerupContainer.addChild(powerupTimerTxt);
// Position container at bottom center, above the very bottom (e.g. 120px up)
powerupContainer.x = 0;
powerupContainer.y = -120;
LK.gui.bottom.addChild(powerupContainer);
// --- MENU OVERLAY ---
var menuOverlay = new Container();
menuOverlay.interactive = true;
menuOverlay.visible = true;
// Decorative background (cover full screen)
var menuBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
scaleX: 30,
scaleY: 30,
tint: 0x1a6cff,
// blue background to match game
alpha: 0.92
});
menuOverlay.addChild(menuBg);
// (Removed decorative accent that appeared as a light green square in the center)
// (Removed menuTitleShadow and menuTitle for logo insertion)
// Add Subway Dashers logo image to menu overlay (top center)
var logoImg = LK.getAsset('subwayDashersLogo', {
anchorX: 0.5,
anchorY: 0,
x: 0,
y: -1200,
scaleX: 7.2,
scaleY: 7.2
});
menuOverlay.addChild(logoImg);
// (Subtitle removed)
// Play button with modern glassy background and shadow (move to bottom center of menu)
var playBtnBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 700,
scaleX: 5.2,
scaleY: 2.2,
tint: 0xffffff,
alpha: 0.18
});
menuOverlay.addChild(playBtnBg);
var playBtnShadow = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 710,
scaleX: 5.2,
scaleY: 2.2,
tint: 0x000000,
alpha: 0.13
});
menuOverlay.addChild(playBtnShadow);
var playBtnAccent = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 700,
scaleX: 4.7,
scaleY: 1.7,
// Use a more orange color
tint: 0xFF8C1A,
alpha: 0.97
});
menuOverlay.addChild(playBtnAccent);
var playBtn = new Text2('START', {
size: 120,
fill: "#fff",
font: "Impact, 'Arial Black', Tahoma",
alpha: 0.98,
shadow: {
color: "#000",
blur: 12,
x: 0,
y: 8
}
});
playBtn.anchor.set(0.5, 0.5);
playBtn.x = 0;
playBtn.y = 700;
menuOverlay.addChild(playBtn);
// Add a subtle divider line (move above play button)
var divider = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 520,
scaleX: 2.8,
scaleY: 0.09,
tint: 0xffffff,
alpha: 0.18
});
menuOverlay.addChild(divider);
// Add a subtle glassy highlight above the button for polish (move above play button)
var menuHighlight = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 540,
scaleX: 2.2,
scaleY: 0.22,
tint: 0xffffff,
alpha: 0.13
});
menuOverlay.addChild(menuHighlight);
// Decorative hint text, more modern (move further below play button)
var hintTxt = new Text2('Swipe left/right to move ยท Swipe up to jump', {
size: 54,
fill: "#fff",
alpha: 0.82
});
hintTxt.anchor.set(0.5, 0.5);
hintTxt.x = 0;
hintTxt.y = 950; // moved further down
menuOverlay.addChild(hintTxt);
// Add a soft drop shadow under the menu for depth
var menuDropShadow = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 420,
scaleX: 7.5,
scaleY: 1.1,
tint: 0x000000,
alpha: 0.10
});
menuOverlay.addChild(menuDropShadow);
// Center overlay horizontally and vertically for full screen coverage
menuOverlay.x = 2048 / 2;
menuOverlay.y = 2732 / 2;
// Add to stage
game.addChild(menuOverlay);
// --- Initialize runner ---
runner = new Runner();
runner.x = laneX(1);
runner.y = runnerY();
game.addChild(runner);
// Hide runner and UI until game starts
runner.visible = false;
scoreTxt.visible = false;
coinTxt.visible = false;
powerupContainer.visible = false;
// --- Touch/Swipe controls ---
var gameStarted = false;
// Block all input until Play is pressed
game.down = function (x, y, obj) {
if (!gameStarted) return;
swipeStartX = x;
swipeStartY = y;
swipeStartTime = LK.ticks;
lastSwipeX = x;
lastSwipeY = y;
};
game.move = function (x, y, obj) {
if (!gameStarted) return;
if (swipeStartX === null) return;
var dx = x - swipeStartX;
var dy = y - swipeStartY;
var absDx = Math.abs(dx);
var absDy = Math.abs(dy);
var swipeThreshold = 120;
var swipeTime = LK.ticks - swipeStartTime;
if (swipeTime > 36) {
// Too slow, reset
swipeStartX = null;
swipeStartY = null;
return;
}
if (absDx > absDy && absDx > swipeThreshold) {
// Horizontal swipe
if (dx > 0 && runner.lane < 2) {
runner.moveToLane(runner.lane + 1);
} else if (dx < 0 && runner.lane > 0) {
runner.moveToLane(runner.lane - 1);
}
swipeStartX = null;
swipeStartY = null;
} else if (absDy > absDx && dy < -swipeThreshold) {
// Upward swipe (jump)
runner.jump();
swipeStartX = null;
swipeStartY = null;
}
};
game.up = function (x, y, obj) {
if (!gameStarted) return;
swipeStartX = null;
swipeStartY = null;
};
function startGame() {
if (gameStarted) return;
gameStarted = true;
menuOverlay.visible = false;
runner.visible = true;
scoreTxt.visible = true;
coinTxt.visible = true;
powerupContainer.visible = true;
// Reset state
isGameOver = false;
score = 0;
coinScore = 0;
speed = 10;
ticks = 0;
invincibleTicks = 0;
magnetTicks = 0;
scoreTxt.setText('0');
coinTxt.setText('0');
powerupTxt.setText('');
powerupTimerTxt.setText('');
}
// Play button interaction
playBtn.interactive = true;
playBtn.down = function (x, y, obj) {
startGame();
};
// Remove tap-anywhere-to-start: only play button starts the game
menuOverlay.down = function (x, y, obj) {
// Do nothing, only playBtn starts the game
};
game.down = function (x, y, obj) {
if (!gameStarted) return;
swipeStartX = x;
swipeStartY = y;
swipeStartTime = LK.ticks;
lastSwipeX = x;
lastSwipeY = y;
};
game.move = function (x, y, obj) {
if (!gameStarted) return;
if (swipeStartX === null) return;
var dx = x - swipeStartX;
var dy = y - swipeStartY;
var absDx = Math.abs(dx);
var absDy = Math.abs(dy);
var swipeThreshold = 120;
var swipeTime = LK.ticks - swipeStartTime;
if (swipeTime > 36) {
// Too slow, reset
swipeStartX = null;
swipeStartY = null;
return;
}
if (absDx > absDy && absDx > swipeThreshold) {
// Horizontal swipe
if (dx > 0 && runner.lane < 2) {
runner.moveToLane(runner.lane + 1);
} else if (dx < 0 && runner.lane > 0) {
runner.moveToLane(runner.lane - 1);
}
swipeStartX = null;
swipeStartY = null;
} else if (absDy > absDx && dy < -swipeThreshold) {
// Upward swipe (jump)
runner.jump();
swipeStartX = null;
swipeStartY = null;
}
};
game.up = function (x, y, obj) {
if (!gameStarted) return;
swipeStartX = null;
swipeStartY = null;
};
// Show menu again on game over
game.on('destroy', function () {
menuOverlay.visible = true;
runner.visible = false;
scoreTxt.visible = false;
coinTxt.visible = false;
powerupContainer.visible = false;
gameStarted = false;
});
// --- Helper: spawn obstacle ---
function spawnObstacle() {
var obs = new Obstacle();
obs.lane = Math.floor(Math.random() * 3);
obs.x = laneX(obs.lane);
obs.y = -120;
var type = Math.random() < 0.7 ? 'barrier' : 'train';
obs.setType(type);
obs.speed = speed;
obstacles.push(obs);
game.addChild(obs);
}
// --- Helper: spawn coin ---
function spawnCoin() {
var coin = new Coin();
coin.lane = Math.floor(Math.random() * 3);
coin.x = laneX(coin.lane);
coin.y = -80;
coin.speed = speed;
coins.push(coin);
game.addChild(coin);
}
// --- Helper: spawn powerup ---
function spawnPowerup() {
var powerup = new Powerup();
powerup.lane = Math.floor(Math.random() * 3);
powerup.x = laneX(powerup.lane);
powerup.y = -100;
powerup.speed = speed;
powerup.kind = Math.random() < 0.5 ? 'invincible' : 'magnet';
powerups.push(powerup);
game.addChild(powerup);
}
// --- Main update loop ---
game.update = function () {
if (!gameStarted) return;
if (isGameOver) return;
ticks++;
// Increase speed over time
// Old: every 180 ticks, +1.2, max 38
// New: every 180 ticks, +0.6, max 38 (so it takes twice as long to reach max speed)
if (ticks % 180 === 0 && speed < 38) {
speed += 0.6;
}
// Update runner
runner.update();
// Update powerup timers
if (invincibleTicks > 0) {
invincibleTicks--;
powerupTxt.setText("INVINCIBLE " + Math.ceil(invincibleTicks / 60).toString() + "s");
powerupTimerTxt.setText("");
} else if (magnetTicks > 0) {
magnetTicks--;
powerupTxt.setText("MAGNET " + Math.ceil(magnetTicks / 60).toString() + "s");
powerupTimerTxt.setText("");
} else {
powerupTxt.setText("");
powerupTimerTxt.setText("");
}
// Spawn obstacles
// 25% more obstacles: reduce interval by 25%
if (ticks % Math.max(36, Math.floor((120 - speed * 2) * 0.75)) === 0) {
spawnObstacle();
}
// Spawn coins
if (ticks % 36 === 0) {
spawnCoin();
}
// Spawn powerups
if (ticks % 420 === 0) {
spawnPowerup();
}
// --- Update obstacles ---
for (var i = obstacles.length - 1; i >= 0; i--) {
var obs = obstacles[i];
obs.speed = speed;
obs.update();
// Remove if off screen
if (obs.y > 2732 + 200) {
obs.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with runner
var collides = false;
if (obs.lane === runner.lane) {
// If runner is jumping, only collide with trains (tall obstacles)
if (obs.type === 'train') {
// Trains are tall, can hit in air
if (Math.abs(obs.y - runner.y) < 180) {
collides = true;
}
} else {
// Barriers: only if runner is not jumping
if (!runner.isJumping && Math.abs(obs.y - runner.y) < 160) {
collides = true;
}
}
}
if (collides && invincibleTicks === 0) {
// Game over
LK.effects.flashScreen(0xff0000, 800);
isGameOver = true;
LK.setScore(score);
LK.showGameOver();
return;
}
}
// --- Update coins ---
for (var j = coins.length - 1; j >= 0; j--) {
var coin = coins[j];
coin.speed = speed;
coin.update();
// Remove if off screen
if (coin.y > 2732 + 100) {
coin.destroy();
coins.splice(j, 1);
continue;
}
// Collect coin
var collect = false;
if (magnetTicks > 0) {
// Magnet: collect if in any lane and close
if (Math.abs(coin.y - runner.y) < 220) {
collect = true;
}
} else {
if (coin.lane === runner.lane && Math.abs(coin.y - runner.y) < 120) {
collect = true;
}
}
if (collect) {
coinScore++;
score += 10;
coinTxt.setText(coinScore);
scoreTxt.setText(score);
coin.destroy();
coins.splice(j, 1);
}
}
// --- Update powerups ---
for (var k = powerups.length - 1; k >= 0; k--) {
var p = powerups[k];
p.speed = speed;
p.update();
// Remove if off screen
if (p.y > 2732 + 120) {
p.destroy();
powerups.splice(k, 1);
continue;
}
// Collect powerup
var collectP = false;
if (p.lane === runner.lane && Math.abs(p.y - runner.y) < 140) {
collectP = true;
}
if (collectP) {
if (p.kind === 'invincible') {
invincibleTicks = 180; // 3 seconds
LK.effects.flashObject(runner, 0x83de44, 800);
} else if (p.kind === 'magnet') {
magnetTicks = 240; // 4 seconds
LK.effects.flashObject(runner, 0xf7d038, 800);
}
p.destroy();
powerups.splice(k, 1);
}
}
// --- Score increases over time ---
if (ticks % 12 === 0) {
score++;
scoreTxt.setText(score);
}
};
// --- Reset state on game over ---
game.on('destroy', function () {
// Clean up arrays
for (var i = 0; i < obstacles.length; i++) obstacles[i].destroy();
for (var j = 0; j < coins.length; j++) coins[j].destroy();
for (var k = 0; k < powerups.length; k++) powerups[k].destroy();
obstacles = [];
coins = [];
powerups = [];
isGameOver = false;
score = 0;
coinScore = 0;
speed = 18;
ticks = 0;
invincibleTicks = 0;
magnetTicks = 0;
scoreTxt.setText('0');
coinTxt.setText('0');
powerupTxt.setText('');
});
2d coin pixel art. In-Game asset. 2d. High contrast. No shadows
2d star pixel art green. In-Game asset. 2d. High contrast. No shadows
2d rail upside view. In-Game asset. 2d. High contrast. No shadows
2d pixel art obstacle upview. In-Game asset. 2d. High contrast. No shadows
subway surfers style logo with text subway dashers. In-Game asset. 2d. High contrast. No shadows