/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Coin
var Coin = Container.expand(function () {
var self = Container.call(this);
var coinGfx = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5
});
// Lane: 0,1,2
self.lane = 1;
// For collision detection
self.isCoin = true;
return self;
});
// UI Elements class for start menu and instructions
var GameUI = Container.expand(function () {
var self = Container.call(this);
// (Title removed)
// Create start button
self.startBtn = new Container();
var startBtnText = new Text2('START GAME', {
size: 100,
fill: 0xffffff
});
startBtnText.anchor.set(0.5, 0.5);
self.startBtn.addChild(startBtnText);
self.startBtn.x = 2048 / 2;
self.startBtn.y = 1200;
self.startBtn.interactive = true;
self.addChild(self.startBtn);
// Create how to play button
self.instructionsBtn = new Container();
var instBtnText = new Text2('HOW TO PLAY', {
size: 100,
fill: 0xffffff
});
instBtnText.anchor.set(0.5, 0.5);
self.instructionsBtn.addChild(instBtnText);
self.instructionsBtn.x = 2048 / 2;
self.instructionsBtn.y = 1400;
self.instructionsBtn.interactive = true;
self.addChild(self.instructionsBtn);
// Create instructions screen (initially hidden)
self.instructions = new Container();
self.instructions.visible = false;
var instTitle = new Text2('HOW TO PLAY', {
size: 120,
fill: 0xFFFFFF
});
instTitle.anchor.set(0.5, 0);
instTitle.x = 2048 / 2;
instTitle.y = 600;
self.instructions.addChild(instTitle);
var instText1 = new Text2('• SWIPE LEFT/RIGHT to change lanes', {
size: 80,
fill: 0xFFFFFF
});
instText1.anchor.set(0.5, 0);
instText1.x = 2048 / 2;
instText1.y = 800;
self.instructions.addChild(instText1);
var instText2 = new Text2('• Avoid obstacles by changing lanes', {
size: 80,
fill: 0xFFFFFF
});
instText2.anchor.set(0.5, 0);
instText2.x = 2048 / 2;
instText2.y = 900;
self.instructions.addChild(instText2);
var instText4 = new Text2('• Collect coins for bonus points', {
size: 80,
fill: 0xFFFFFF
});
instText4.anchor.set(0.5, 0);
instText4.x = 2048 / 2;
instText4.y = 1000;
self.instructions.addChild(instText4);
// Back button for instructions
self.backBtn = new Container();
var backText = new Text2('BACK', {
size: 80,
fill: 0xFFFFFF
});
backText.anchor.set(0.5, 0.5);
self.backBtn.addChild(backText);
self.backBtn.x = 2048 / 2;
self.backBtn.y = 1400;
self.backBtn.interactive = true;
self.instructions.addChild(self.backBtn);
self.addChild(self.instructions);
// Button press handlers
self.startBtn.down = function () {
tween(self.startBtn, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 100
});
};
self.startBtn.up = function () {
tween(self.startBtn, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
onFinish: function onFinish() {
if (typeof self.onStartClick === 'function') {
self.onStartClick();
}
}
});
};
self.instructionsBtn.down = function () {
tween(self.instructionsBtn, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 100
});
};
self.instructionsBtn.up = function () {
tween(self.instructionsBtn, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
onFinish: function onFinish() {
if (typeof self.onInstructionsClick === 'function') {
self.onInstructionsClick();
}
}
});
};
self.backBtn.down = function () {
tween(self.backBtn, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 100
});
};
self.backBtn.up = function () {
tween(self.backBtn, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
onFinish: function onFinish() {
if (typeof self.onBackClick === 'function') {
self.onBackClick();
}
}
});
};
// Set show/hide methods
self.showMenu = function () {
self.visible = true;
self.startBtn.visible = true;
self.instructionsBtn.visible = true;
self.instructions.visible = false;
};
self.showInstructions = function () {
self.visible = true;
self.startBtn.visible = false;
self.instructionsBtn.visible = false;
self.instructions.visible = true;
};
self.hide = function () {
self.visible = false;
};
return self;
});
// Obstacle
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obsGfx = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
// Lane: 0,1,2
self.lane = 1;
// For collision detection
self.isObstacle = true;
return self;
});
// Runner (player character)
var Runner = Container.expand(function () {
var self = Container.call(this);
var runnerGfx = self.attachAsset('runner', {
anchorX: 0.5,
anchorY: 0.5
});
// Current lane: 0 (left), 1 (center), 2 (right)
self.lane = 1;
self.moveToLane = function (lane, duration) {
self.lane = lane;
var targetX = laneToX(lane);
tween(self, {
x: targetX
}, {
duration: duration || 120,
easing: tween.cubicOut,
onFinish: function onFinish() {
// (jumpIndicator logic removed)
}
});
};
// Called every frame by game.update
self.update = function () {};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Button image assets
// --- ROAD BACKGROUND ---
// Character: Red box
// Obstacle: Gray box
// Coin: Yellow ellipse
var roadBg1 = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0
});
roadBg1.x = 2048 / 2;
roadBg1.y = 0;
game.addChild(roadBg1);
var roadBg2 = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0
});
roadBg2.x = 2048 / 2;
roadBg2.y = -2732;
game.addChild(roadBg2);
// --- LANE SYSTEM ---
var NUM_LANES = 3;
var LANE_WIDTH = 2048 / NUM_LANES; // Use full width
var LANE_START_X = LANE_WIDTH / 2; // Center of first lane at left edge
function laneToX(lane) {
// lane: 0 (left), 1 (center), 2 (right)
return LANE_START_X + lane * LANE_WIDTH;
}
// Game state manager
var GameState = {
MENU: 0,
INSTRUCTIONS: 1,
PLAYING: 2
};
// --- GAMEPLAY VARIABLES ---
var runner;
var obstacles = [];
var coinObjects = []; // Array for coin instances
var speed = 18; // Initial speed (pixels per frame)
var speedIncrease = 0.012; // Speed up per tick
var spawnTimer = 0;
var coinSpawnTimer = 0;
var minObstacleGap = 420; // Minimum vertical gap between obstacles
var lastObstacleY = 0;
var isGameOver = false;
// Coin system variables
var coinCombo = 0;
var coinComboTimer = 0;
var coinComboTimeout = 60; // frames (1 second) to maintain combo
var coinComboText = null;
// Game state management
var currentState = GameState.MENU;
var gameUI = new GameUI();
game.addChild(gameUI);
// Setup UI event handlers
gameUI.onStartClick = function () {
currentState = GameState.PLAYING;
gameUI.hide();
resetGame();
// Show score and coin counter
scoreTxt.visible = true;
showCoinCounterAnimated();
};
gameUI.onInstructionsClick = function () {
currentState = GameState.INSTRUCTIONS;
gameUI.showInstructions();
};
gameUI.onBackClick = function () {
currentState = GameState.MENU;
gameUI.showMenu();
coinCounter.visible = false;
};
// Initial setup
gameUI.showMenu();
// --- SCORE AND COINS ---
var score = 0;
var coins = 0; // Count of collected coins
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Hide score initially
scoreTxt.visible = false;
// Coin counter display
var coinCounter = new Container();
var coinIcon = LK.getAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
coinIcon.x = 60;
coinCounter.addChild(coinIcon);
var coinsTxt = new Text2('0', {
size: 100,
fill: 0xFFCC00
});
coinsTxt.anchor.set(0, 0.5);
coinsTxt.x = coinIcon.x + 80;
coinsTxt.y = coinIcon.y;
coinCounter.addChild(coinsTxt);
// Position the coin counter at the top left (avoid 0,0 for menu icon, so use x=120)
coinCounter.x = 120;
coinCounter.y = 120;
LK.gui.topLeft.addChild(coinCounter);
// Hide coin counter initially
coinCounter.visible = false;
// Animate coin counter entrance when shown
function showCoinCounterAnimated() {
coinCounter.visible = true;
coinCounter.alpha = 0;
coinCounter.y = 60; // Start slightly above
tween(coinCounter, {
alpha: 1,
y: 120
}, {
duration: 400,
easing: tween.bounceOut
});
}
// Function to reset game state
function resetGame() {
// Clean up arrays
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
for (var j = 0; j < coinObjects.length; j++) {
coinObjects[j].destroy();
}
obstacles = [];
coinObjects = [];
isGameOver = false;
speed = 18;
score = 0;
coins = 0;
coinCombo = 0;
coinComboTimer = 0;
if (coinComboText) {
coinComboText.destroy();
coinComboText = null;
}
LK.setScore(0);
scoreTxt.setText('0');
coinsTxt.setText('0');
runner.x = laneToX(1);
runner.lane = 1;
runner.y = 2732 - 400;
runner.isJumping = false;
spawnTimer = 0;
coinSpawnTimer = 0;
roadBg1.y = 0;
roadBg2.y = -2732;
}
// --- INIT RUNNER ---
runner = new Runner();
runner.y = 2732 - 400;
runner.x = laneToX(1);
runner.lane = 1;
game.addChild(runner);
// --- INPUT HANDLING ---
var dragStartX = null;
var dragStartLane = null;
var dragActive = false;
// For jump hold logic
function handleMove(x, y, obj) {
if (currentState !== GameState.PLAYING) return;
if (dragActive && dragStartX !== null) {
var dx = x - dragStartX;
if (Math.abs(dx) > 120) {
// Swipe detected
if (dx > 0 && runner.lane < NUM_LANES - 1) {
runner.moveToLane(runner.lane + 1);
dragStartX = x;
} else if (dx < 0 && runner.lane > 0) {
runner.moveToLane(runner.lane - 1);
dragStartX = x;
}
}
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
if (currentState !== GameState.PLAYING) return;
dragActive = true;
dragStartX = x;
dragStartLane = runner.lane;
};
game.up = function (x, y, obj) {
if (currentState !== GameState.PLAYING) return;
dragActive = false;
dragStartX = null;
dragStartLane = null;
};
// --- TAP TO MOVE (for single taps) ---
game.tap = function (x, y, obj) {
// Not supported by LK, so handled via down/up quick tap
// (No implementation needed)
};
// --- SPAWN OBSTACLES & COINS ---
function spawnObstacle() {
var lane = Math.floor(Math.random() * NUM_LANES);
var obs = new Obstacle();
obs.lane = lane;
obs.x = laneToX(lane);
obs.y = -180;
obstacles.push(obs);
game.addChild(obs);
lastObstacleY = obs.y;
}
function spawnCoin() {
// Prevent coins from spawning in the same lane and too close to obstacles, and at the same Y as obstacles
var availableLanes = [0, 1, 2];
var coinSpawnY = -120;
// Remove lanes with obstacles too close to spawn Y or at the same Y
for (var i = 0; i < obstacles.length; i++) {
var obs = obstacles[i];
// If obstacle is in the same lane and within 320px vertically of coin spawn Y (-120)
if (Math.abs(obs.y - coinSpawnY) < 320) {
var idx = availableLanes.indexOf(obs.lane);
if (idx !== -1) availableLanes.splice(idx, 1);
}
// Prevent coin from spawning at the same Y as any obstacle (even in other lanes)
if (Math.abs(obs.y - coinSpawnY) < 1) {
// If any obstacle is at the same Y, skip coin spawn entirely
return;
}
}
if (availableLanes.length === 0) return; // No safe lane, skip coin spawn
var lane = availableLanes[Math.floor(Math.random() * availableLanes.length)];
var coin = new Coin();
coin.lane = lane;
coin.x = laneToX(lane);
coin.y = coinSpawnY;
coinObjects.push(coin);
game.addChild(coin);
}
// --- COLLISION DETECTION ---
function checkCollision(a, b) {
// Simple AABB
var dx = Math.abs(a.x - b.x);
var dy = Math.abs(a.y - b.y);
var aw = a.width || 180,
ah = a.height || 180;
var bw = b.width || 180,
bh = b.height || 180;
return dx < aw / 2 + bw / 2 - 20 && dy < ah / 2 + bh / 2 - 20;
}
// --- GAME LOOP ---
game.update = function () {
if (isGameOver || currentState !== GameState.PLAYING) return;
// --- Runner jump update ---
if (runner && typeof runner.update === "function") {
runner.update();
}
// Scroll road background
roadBg1.y += speed;
roadBg2.y += speed;
// Loop backgrounds
if (roadBg1.y >= 2732) {
roadBg1.y = roadBg2.y - 2732;
}
if (roadBg2.y >= 2732) {
roadBg2.y = roadBg1.y - 2732;
}
// Increase speed
speed += speedIncrease;
// Handle coin combo timer
if (coinCombo > 0) {
coinComboTimer--;
if (coinComboTimer <= 0) {
// Reset combo when timer expires
coinCombo = 0;
if (coinComboText) {
coinComboText.destroy();
coinComboText = null;
}
}
}
// Move obstacles
for (var i = obstacles.length - 1; i >= 0; i--) {
var obs = obstacles[i];
obs.y += speed;
if (obs.y > 2732 + 200) {
obs.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with runner
if (checkCollision(obs, runner) && !runner.isJumping) {
triggerGameOver();
return;
}
}
// Move coins
for (var j = coinObjects.length - 1; j >= 0; j--) {
var coin = coinObjects[j];
coin.y += speed;
if (coin.y > 2732 + 120) {
coin.destroy();
coinObjects.splice(j, 1);
continue;
}
// Collision with runner
if (checkCollision(coin, runner)) {
// Increase combo counter
coinCombo++;
coinComboTimer = coinComboTimeout;
// Calculate bonus based on combo
var bonus = Math.min(coinCombo, 5); // Cap bonus at 5x
var coinValue = 10 * bonus;
// Collect coin
coins += 1;
score += coinValue;
LK.setScore(score);
scoreTxt.setText(score);
coinsTxt.setText(coins);
// Show combo text
if (coinComboText) {
coinComboText.destroy();
}
if (coinCombo > 1) {
coinComboText = new Text2('x' + coinCombo + ' COMBO!', {
size: 80,
fill: 0xFFCC00
});
coinComboText.anchor.set(0.5, 0.5);
coinComboText.x = coin.x;
coinComboText.y = coin.y - 100;
game.addChild(coinComboText);
// Animate combo text
tween(coinComboText, {
y: coinComboText.y - 100,
alpha: 0
}, {
duration: 1000,
easing: tween.cubicOut,
onFinish: function onFinish() {
if (coinComboText) {
coinComboText.destroy();
coinComboText = null;
}
}
});
}
// Add sparkle effect
LK.effects.flashObject(coin, 0xffff00, 200);
// Play coin sound
LK.getSound('coin_collect').play();
// Play a bounce animation on the coin counter
tween(coinIcon, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 150,
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(coinIcon, {
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 150,
easing: tween.easeOut
});
}
});
coin.destroy();
coinObjects.splice(j, 1);
continue;
}
}
// Spawn obstacles
spawnTimer -= speed;
if (spawnTimer <= 0) {
spawnObstacle();
// Next spawn in 700-1100 px
spawnTimer = minObstacleGap + Math.floor(Math.random() * 400);
}
// Spawn coins
coinSpawnTimer -= speed;
if (coinSpawnTimer <= 0) {
spawnCoin();
// Next coin in 400-900 px
coinSpawnTimer = 400 + Math.floor(Math.random() * 500);
}
};
// --- CUSTOM GAME OVER SCREEN ---
var gameOverScreen = new Container();
gameOverScreen.visible = false;
// Dimmed background
var gameOverBg = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2
});
gameOverBg.x = 2048 / 2;
gameOverBg.y = 2732 / 2;
gameOverBg.alpha = 0.7;
gameOverBg.tint = 0x222222;
gameOverScreen.addChild(gameOverBg);
// "Game Over" text
var gameOverText = new Text2('GAME OVER', {
size: 180,
fill: 0xFF4444
});
gameOverText.anchor.set(0.5, 0.5);
gameOverText.x = 2048 / 2;
gameOverText.y = 800;
gameOverScreen.addChild(gameOverText);
// Score label
var finalScoreText = new Text2('SCORE: 0', {
size: 120,
fill: 0xffffff
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 1100;
gameOverScreen.addChild(finalScoreText);
// Coins label
var finalCoinsText = new Text2('COINS: 0', {
size: 100,
fill: 0xFFCC00
});
finalCoinsText.anchor.set(0.5, 0.5);
finalCoinsText.x = 2048 / 2;
finalCoinsText.y = 1250;
gameOverScreen.addChild(finalCoinsText);
// Restart button
var restartBtn = new Container();
var restartBtnText = new Text2('RESTART', {
size: 140,
fill: 0xffffff
});
restartBtnText.anchor.set(0.5, 0.5);
restartBtn.addChild(restartBtnText);
restartBtn.x = 2048 / 2;
restartBtn.y = 1500;
restartBtn.interactive = true;
gameOverScreen.addChild(restartBtn);
game.addChild(gameOverScreen);
// Animate in the game over screen
function showGameOverScreen() {
finalScoreText.setText('SCORE: ' + score);
finalCoinsText.setText('COINS: ' + coins);
gameOverScreen.visible = true;
gameOverScreen.alpha = 0;
// Hide gameplay elements
runner.visible = false;
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].visible = false;
}
for (var j = 0; j < coinObjects.length; j++) {
coinObjects[j].visible = false;
}
roadBg1.visible = false;
roadBg2.visible = false;
scoreTxt.visible = false;
coinCounter.visible = false;
tween(gameOverScreen, {
alpha: 1
}, {
duration: 400,
easing: tween.cubicOut
});
}
// Hide the game over screen
function hideGameOverScreen() {
gameOverScreen.visible = false;
// Restore gameplay elements
runner.visible = true;
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].visible = true;
}
for (var j = 0; j < coinObjects.length; j++) {
coinObjects[j].visible = true;
}
roadBg1.visible = true;
roadBg2.visible = true;
}
// Restart button handlers
restartBtn.down = function () {
tween(restartBtn, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 100
});
};
restartBtn.up = function () {
tween(restartBtn, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
onFinish: function onFinish() {
hideGameOverScreen();
currentState = GameState.PLAYING;
gameUI.hide();
resetGame();
scoreTxt.visible = true;
showCoinCounterAnimated();
}
});
};
// --- RESET 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 < coinObjects.length; j++) {
coinObjects[j].destroy();
}
obstacles = [];
coinObjects = [];
isGameOver = false;
speed = 18;
score = 0;
coins = 0;
coinCombo = 0;
coinComboTimer = 0;
if (coinComboText) {
coinComboText.destroy();
coinComboText = null;
}
LK.setScore(0);
scoreTxt.setText('0');
coinsTxt.setText('0');
runner.x = laneToX(1);
runner.lane = 1;
runner.y = 2732 - 400;
spawnTimer = 0;
coinSpawnTimer = 0;
roadBg1.y = 0;
roadBg2.y = -2732;
// Return to menu
currentState = GameState.MENU;
gameUI.showMenu();
// Hide game UI
scoreTxt.visible = false;
coinCounter.visible = false;
hideGameOverScreen();
});
// Show custom game over screen instead of default
function triggerGameOver() {
isGameOver = true;
LK.effects.flashScreen(0xff0000, 800);
showGameOverScreen();
} /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Coin
var Coin = Container.expand(function () {
var self = Container.call(this);
var coinGfx = self.attachAsset('coin', {
anchorX: 0.5,
anchorY: 0.5
});
// Lane: 0,1,2
self.lane = 1;
// For collision detection
self.isCoin = true;
return self;
});
// UI Elements class for start menu and instructions
var GameUI = Container.expand(function () {
var self = Container.call(this);
// (Title removed)
// Create start button
self.startBtn = new Container();
var startBtnText = new Text2('START GAME', {
size: 100,
fill: 0xffffff
});
startBtnText.anchor.set(0.5, 0.5);
self.startBtn.addChild(startBtnText);
self.startBtn.x = 2048 / 2;
self.startBtn.y = 1200;
self.startBtn.interactive = true;
self.addChild(self.startBtn);
// Create how to play button
self.instructionsBtn = new Container();
var instBtnText = new Text2('HOW TO PLAY', {
size: 100,
fill: 0xffffff
});
instBtnText.anchor.set(0.5, 0.5);
self.instructionsBtn.addChild(instBtnText);
self.instructionsBtn.x = 2048 / 2;
self.instructionsBtn.y = 1400;
self.instructionsBtn.interactive = true;
self.addChild(self.instructionsBtn);
// Create instructions screen (initially hidden)
self.instructions = new Container();
self.instructions.visible = false;
var instTitle = new Text2('HOW TO PLAY', {
size: 120,
fill: 0xFFFFFF
});
instTitle.anchor.set(0.5, 0);
instTitle.x = 2048 / 2;
instTitle.y = 600;
self.instructions.addChild(instTitle);
var instText1 = new Text2('• SWIPE LEFT/RIGHT to change lanes', {
size: 80,
fill: 0xFFFFFF
});
instText1.anchor.set(0.5, 0);
instText1.x = 2048 / 2;
instText1.y = 800;
self.instructions.addChild(instText1);
var instText2 = new Text2('• Avoid obstacles by changing lanes', {
size: 80,
fill: 0xFFFFFF
});
instText2.anchor.set(0.5, 0);
instText2.x = 2048 / 2;
instText2.y = 900;
self.instructions.addChild(instText2);
var instText4 = new Text2('• Collect coins for bonus points', {
size: 80,
fill: 0xFFFFFF
});
instText4.anchor.set(0.5, 0);
instText4.x = 2048 / 2;
instText4.y = 1000;
self.instructions.addChild(instText4);
// Back button for instructions
self.backBtn = new Container();
var backText = new Text2('BACK', {
size: 80,
fill: 0xFFFFFF
});
backText.anchor.set(0.5, 0.5);
self.backBtn.addChild(backText);
self.backBtn.x = 2048 / 2;
self.backBtn.y = 1400;
self.backBtn.interactive = true;
self.instructions.addChild(self.backBtn);
self.addChild(self.instructions);
// Button press handlers
self.startBtn.down = function () {
tween(self.startBtn, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 100
});
};
self.startBtn.up = function () {
tween(self.startBtn, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
onFinish: function onFinish() {
if (typeof self.onStartClick === 'function') {
self.onStartClick();
}
}
});
};
self.instructionsBtn.down = function () {
tween(self.instructionsBtn, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 100
});
};
self.instructionsBtn.up = function () {
tween(self.instructionsBtn, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
onFinish: function onFinish() {
if (typeof self.onInstructionsClick === 'function') {
self.onInstructionsClick();
}
}
});
};
self.backBtn.down = function () {
tween(self.backBtn, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 100
});
};
self.backBtn.up = function () {
tween(self.backBtn, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
onFinish: function onFinish() {
if (typeof self.onBackClick === 'function') {
self.onBackClick();
}
}
});
};
// Set show/hide methods
self.showMenu = function () {
self.visible = true;
self.startBtn.visible = true;
self.instructionsBtn.visible = true;
self.instructions.visible = false;
};
self.showInstructions = function () {
self.visible = true;
self.startBtn.visible = false;
self.instructionsBtn.visible = false;
self.instructions.visible = true;
};
self.hide = function () {
self.visible = false;
};
return self;
});
// Obstacle
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obsGfx = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
// Lane: 0,1,2
self.lane = 1;
// For collision detection
self.isObstacle = true;
return self;
});
// Runner (player character)
var Runner = Container.expand(function () {
var self = Container.call(this);
var runnerGfx = self.attachAsset('runner', {
anchorX: 0.5,
anchorY: 0.5
});
// Current lane: 0 (left), 1 (center), 2 (right)
self.lane = 1;
self.moveToLane = function (lane, duration) {
self.lane = lane;
var targetX = laneToX(lane);
tween(self, {
x: targetX
}, {
duration: duration || 120,
easing: tween.cubicOut,
onFinish: function onFinish() {
// (jumpIndicator logic removed)
}
});
};
// Called every frame by game.update
self.update = function () {};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Button image assets
// --- ROAD BACKGROUND ---
// Character: Red box
// Obstacle: Gray box
// Coin: Yellow ellipse
var roadBg1 = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0
});
roadBg1.x = 2048 / 2;
roadBg1.y = 0;
game.addChild(roadBg1);
var roadBg2 = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0
});
roadBg2.x = 2048 / 2;
roadBg2.y = -2732;
game.addChild(roadBg2);
// --- LANE SYSTEM ---
var NUM_LANES = 3;
var LANE_WIDTH = 2048 / NUM_LANES; // Use full width
var LANE_START_X = LANE_WIDTH / 2; // Center of first lane at left edge
function laneToX(lane) {
// lane: 0 (left), 1 (center), 2 (right)
return LANE_START_X + lane * LANE_WIDTH;
}
// Game state manager
var GameState = {
MENU: 0,
INSTRUCTIONS: 1,
PLAYING: 2
};
// --- GAMEPLAY VARIABLES ---
var runner;
var obstacles = [];
var coinObjects = []; // Array for coin instances
var speed = 18; // Initial speed (pixels per frame)
var speedIncrease = 0.012; // Speed up per tick
var spawnTimer = 0;
var coinSpawnTimer = 0;
var minObstacleGap = 420; // Minimum vertical gap between obstacles
var lastObstacleY = 0;
var isGameOver = false;
// Coin system variables
var coinCombo = 0;
var coinComboTimer = 0;
var coinComboTimeout = 60; // frames (1 second) to maintain combo
var coinComboText = null;
// Game state management
var currentState = GameState.MENU;
var gameUI = new GameUI();
game.addChild(gameUI);
// Setup UI event handlers
gameUI.onStartClick = function () {
currentState = GameState.PLAYING;
gameUI.hide();
resetGame();
// Show score and coin counter
scoreTxt.visible = true;
showCoinCounterAnimated();
};
gameUI.onInstructionsClick = function () {
currentState = GameState.INSTRUCTIONS;
gameUI.showInstructions();
};
gameUI.onBackClick = function () {
currentState = GameState.MENU;
gameUI.showMenu();
coinCounter.visible = false;
};
// Initial setup
gameUI.showMenu();
// --- SCORE AND COINS ---
var score = 0;
var coins = 0; // Count of collected coins
var scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Hide score initially
scoreTxt.visible = false;
// Coin counter display
var coinCounter = new Container();
var coinIcon = LK.getAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
coinIcon.x = 60;
coinCounter.addChild(coinIcon);
var coinsTxt = new Text2('0', {
size: 100,
fill: 0xFFCC00
});
coinsTxt.anchor.set(0, 0.5);
coinsTxt.x = coinIcon.x + 80;
coinsTxt.y = coinIcon.y;
coinCounter.addChild(coinsTxt);
// Position the coin counter at the top left (avoid 0,0 for menu icon, so use x=120)
coinCounter.x = 120;
coinCounter.y = 120;
LK.gui.topLeft.addChild(coinCounter);
// Hide coin counter initially
coinCounter.visible = false;
// Animate coin counter entrance when shown
function showCoinCounterAnimated() {
coinCounter.visible = true;
coinCounter.alpha = 0;
coinCounter.y = 60; // Start slightly above
tween(coinCounter, {
alpha: 1,
y: 120
}, {
duration: 400,
easing: tween.bounceOut
});
}
// Function to reset game state
function resetGame() {
// Clean up arrays
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].destroy();
}
for (var j = 0; j < coinObjects.length; j++) {
coinObjects[j].destroy();
}
obstacles = [];
coinObjects = [];
isGameOver = false;
speed = 18;
score = 0;
coins = 0;
coinCombo = 0;
coinComboTimer = 0;
if (coinComboText) {
coinComboText.destroy();
coinComboText = null;
}
LK.setScore(0);
scoreTxt.setText('0');
coinsTxt.setText('0');
runner.x = laneToX(1);
runner.lane = 1;
runner.y = 2732 - 400;
runner.isJumping = false;
spawnTimer = 0;
coinSpawnTimer = 0;
roadBg1.y = 0;
roadBg2.y = -2732;
}
// --- INIT RUNNER ---
runner = new Runner();
runner.y = 2732 - 400;
runner.x = laneToX(1);
runner.lane = 1;
game.addChild(runner);
// --- INPUT HANDLING ---
var dragStartX = null;
var dragStartLane = null;
var dragActive = false;
// For jump hold logic
function handleMove(x, y, obj) {
if (currentState !== GameState.PLAYING) return;
if (dragActive && dragStartX !== null) {
var dx = x - dragStartX;
if (Math.abs(dx) > 120) {
// Swipe detected
if (dx > 0 && runner.lane < NUM_LANES - 1) {
runner.moveToLane(runner.lane + 1);
dragStartX = x;
} else if (dx < 0 && runner.lane > 0) {
runner.moveToLane(runner.lane - 1);
dragStartX = x;
}
}
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
if (currentState !== GameState.PLAYING) return;
dragActive = true;
dragStartX = x;
dragStartLane = runner.lane;
};
game.up = function (x, y, obj) {
if (currentState !== GameState.PLAYING) return;
dragActive = false;
dragStartX = null;
dragStartLane = null;
};
// --- TAP TO MOVE (for single taps) ---
game.tap = function (x, y, obj) {
// Not supported by LK, so handled via down/up quick tap
// (No implementation needed)
};
// --- SPAWN OBSTACLES & COINS ---
function spawnObstacle() {
var lane = Math.floor(Math.random() * NUM_LANES);
var obs = new Obstacle();
obs.lane = lane;
obs.x = laneToX(lane);
obs.y = -180;
obstacles.push(obs);
game.addChild(obs);
lastObstacleY = obs.y;
}
function spawnCoin() {
// Prevent coins from spawning in the same lane and too close to obstacles, and at the same Y as obstacles
var availableLanes = [0, 1, 2];
var coinSpawnY = -120;
// Remove lanes with obstacles too close to spawn Y or at the same Y
for (var i = 0; i < obstacles.length; i++) {
var obs = obstacles[i];
// If obstacle is in the same lane and within 320px vertically of coin spawn Y (-120)
if (Math.abs(obs.y - coinSpawnY) < 320) {
var idx = availableLanes.indexOf(obs.lane);
if (idx !== -1) availableLanes.splice(idx, 1);
}
// Prevent coin from spawning at the same Y as any obstacle (even in other lanes)
if (Math.abs(obs.y - coinSpawnY) < 1) {
// If any obstacle is at the same Y, skip coin spawn entirely
return;
}
}
if (availableLanes.length === 0) return; // No safe lane, skip coin spawn
var lane = availableLanes[Math.floor(Math.random() * availableLanes.length)];
var coin = new Coin();
coin.lane = lane;
coin.x = laneToX(lane);
coin.y = coinSpawnY;
coinObjects.push(coin);
game.addChild(coin);
}
// --- COLLISION DETECTION ---
function checkCollision(a, b) {
// Simple AABB
var dx = Math.abs(a.x - b.x);
var dy = Math.abs(a.y - b.y);
var aw = a.width || 180,
ah = a.height || 180;
var bw = b.width || 180,
bh = b.height || 180;
return dx < aw / 2 + bw / 2 - 20 && dy < ah / 2 + bh / 2 - 20;
}
// --- GAME LOOP ---
game.update = function () {
if (isGameOver || currentState !== GameState.PLAYING) return;
// --- Runner jump update ---
if (runner && typeof runner.update === "function") {
runner.update();
}
// Scroll road background
roadBg1.y += speed;
roadBg2.y += speed;
// Loop backgrounds
if (roadBg1.y >= 2732) {
roadBg1.y = roadBg2.y - 2732;
}
if (roadBg2.y >= 2732) {
roadBg2.y = roadBg1.y - 2732;
}
// Increase speed
speed += speedIncrease;
// Handle coin combo timer
if (coinCombo > 0) {
coinComboTimer--;
if (coinComboTimer <= 0) {
// Reset combo when timer expires
coinCombo = 0;
if (coinComboText) {
coinComboText.destroy();
coinComboText = null;
}
}
}
// Move obstacles
for (var i = obstacles.length - 1; i >= 0; i--) {
var obs = obstacles[i];
obs.y += speed;
if (obs.y > 2732 + 200) {
obs.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with runner
if (checkCollision(obs, runner) && !runner.isJumping) {
triggerGameOver();
return;
}
}
// Move coins
for (var j = coinObjects.length - 1; j >= 0; j--) {
var coin = coinObjects[j];
coin.y += speed;
if (coin.y > 2732 + 120) {
coin.destroy();
coinObjects.splice(j, 1);
continue;
}
// Collision with runner
if (checkCollision(coin, runner)) {
// Increase combo counter
coinCombo++;
coinComboTimer = coinComboTimeout;
// Calculate bonus based on combo
var bonus = Math.min(coinCombo, 5); // Cap bonus at 5x
var coinValue = 10 * bonus;
// Collect coin
coins += 1;
score += coinValue;
LK.setScore(score);
scoreTxt.setText(score);
coinsTxt.setText(coins);
// Show combo text
if (coinComboText) {
coinComboText.destroy();
}
if (coinCombo > 1) {
coinComboText = new Text2('x' + coinCombo + ' COMBO!', {
size: 80,
fill: 0xFFCC00
});
coinComboText.anchor.set(0.5, 0.5);
coinComboText.x = coin.x;
coinComboText.y = coin.y - 100;
game.addChild(coinComboText);
// Animate combo text
tween(coinComboText, {
y: coinComboText.y - 100,
alpha: 0
}, {
duration: 1000,
easing: tween.cubicOut,
onFinish: function onFinish() {
if (coinComboText) {
coinComboText.destroy();
coinComboText = null;
}
}
});
}
// Add sparkle effect
LK.effects.flashObject(coin, 0xffff00, 200);
// Play coin sound
LK.getSound('coin_collect').play();
// Play a bounce animation on the coin counter
tween(coinIcon, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 150,
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(coinIcon, {
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 150,
easing: tween.easeOut
});
}
});
coin.destroy();
coinObjects.splice(j, 1);
continue;
}
}
// Spawn obstacles
spawnTimer -= speed;
if (spawnTimer <= 0) {
spawnObstacle();
// Next spawn in 700-1100 px
spawnTimer = minObstacleGap + Math.floor(Math.random() * 400);
}
// Spawn coins
coinSpawnTimer -= speed;
if (coinSpawnTimer <= 0) {
spawnCoin();
// Next coin in 400-900 px
coinSpawnTimer = 400 + Math.floor(Math.random() * 500);
}
};
// --- CUSTOM GAME OVER SCREEN ---
var gameOverScreen = new Container();
gameOverScreen.visible = false;
// Dimmed background
var gameOverBg = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2
});
gameOverBg.x = 2048 / 2;
gameOverBg.y = 2732 / 2;
gameOverBg.alpha = 0.7;
gameOverBg.tint = 0x222222;
gameOverScreen.addChild(gameOverBg);
// "Game Over" text
var gameOverText = new Text2('GAME OVER', {
size: 180,
fill: 0xFF4444
});
gameOverText.anchor.set(0.5, 0.5);
gameOverText.x = 2048 / 2;
gameOverText.y = 800;
gameOverScreen.addChild(gameOverText);
// Score label
var finalScoreText = new Text2('SCORE: 0', {
size: 120,
fill: 0xffffff
});
finalScoreText.anchor.set(0.5, 0.5);
finalScoreText.x = 2048 / 2;
finalScoreText.y = 1100;
gameOverScreen.addChild(finalScoreText);
// Coins label
var finalCoinsText = new Text2('COINS: 0', {
size: 100,
fill: 0xFFCC00
});
finalCoinsText.anchor.set(0.5, 0.5);
finalCoinsText.x = 2048 / 2;
finalCoinsText.y = 1250;
gameOverScreen.addChild(finalCoinsText);
// Restart button
var restartBtn = new Container();
var restartBtnText = new Text2('RESTART', {
size: 140,
fill: 0xffffff
});
restartBtnText.anchor.set(0.5, 0.5);
restartBtn.addChild(restartBtnText);
restartBtn.x = 2048 / 2;
restartBtn.y = 1500;
restartBtn.interactive = true;
gameOverScreen.addChild(restartBtn);
game.addChild(gameOverScreen);
// Animate in the game over screen
function showGameOverScreen() {
finalScoreText.setText('SCORE: ' + score);
finalCoinsText.setText('COINS: ' + coins);
gameOverScreen.visible = true;
gameOverScreen.alpha = 0;
// Hide gameplay elements
runner.visible = false;
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].visible = false;
}
for (var j = 0; j < coinObjects.length; j++) {
coinObjects[j].visible = false;
}
roadBg1.visible = false;
roadBg2.visible = false;
scoreTxt.visible = false;
coinCounter.visible = false;
tween(gameOverScreen, {
alpha: 1
}, {
duration: 400,
easing: tween.cubicOut
});
}
// Hide the game over screen
function hideGameOverScreen() {
gameOverScreen.visible = false;
// Restore gameplay elements
runner.visible = true;
for (var i = 0; i < obstacles.length; i++) {
obstacles[i].visible = true;
}
for (var j = 0; j < coinObjects.length; j++) {
coinObjects[j].visible = true;
}
roadBg1.visible = true;
roadBg2.visible = true;
}
// Restart button handlers
restartBtn.down = function () {
tween(restartBtn, {
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 100
});
};
restartBtn.up = function () {
tween(restartBtn, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
onFinish: function onFinish() {
hideGameOverScreen();
currentState = GameState.PLAYING;
gameUI.hide();
resetGame();
scoreTxt.visible = true;
showCoinCounterAnimated();
}
});
};
// --- RESET 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 < coinObjects.length; j++) {
coinObjects[j].destroy();
}
obstacles = [];
coinObjects = [];
isGameOver = false;
speed = 18;
score = 0;
coins = 0;
coinCombo = 0;
coinComboTimer = 0;
if (coinComboText) {
coinComboText.destroy();
coinComboText = null;
}
LK.setScore(0);
scoreTxt.setText('0');
coinsTxt.setText('0');
runner.x = laneToX(1);
runner.lane = 1;
runner.y = 2732 - 400;
spawnTimer = 0;
coinSpawnTimer = 0;
roadBg1.y = 0;
roadBg2.y = -2732;
// Return to menu
currentState = GameState.MENU;
gameUI.showMenu();
// Hide game UI
scoreTxt.visible = false;
coinCounter.visible = false;
hideGameOverScreen();
});
// Show custom game over screen instead of default
function triggerGameOver() {
isGameOver = true;
LK.effects.flashScreen(0xff0000, 800);
showGameOverScreen();
}
Pixel, 2d, Coin. In-Game asset. 2d. High contrast. No shadows
container, 2d, pixel. In-Game asset. 2d. High contrast. No shadows
character with a hat, 2d, pixel,. 2d. High contrast. No shadows
road, pixel, 2d. In-Game asset. 2d. High contrast. No shadows. In-Game asset
indicator, pixel, 2d. In-Game asset. 2d. High contrast. No shadows
Start button, pixel art. In-Game asset. 2d. High contrast. No shadows
how to play button, orange outline, white text, pixel art. In-Game asset. 2d. High contrast. No shadows
restart button, orange outline, white text, pixel art.. In-Game asset. 2d. High contrast. No shadows