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