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