/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Checkpoint Class (for scoring)
var Checkpoint = Container.expand(function () {
var self = Container.call(this);
var cpAsset = self.attachAsset('checkpointLine', {
anchorX: 0.5,
anchorY: 0.5
});
cpAsset.width = 1200;
cpAsset.height = 30;
self.getBounds = function () {
return {
x: self.x - cpAsset.width / 2,
y: self.y - cpAsset.height / 2,
width: cpAsset.width,
height: cpAsset.height
};
};
self.update = function () {
self.y += checkpointSpeed;
};
return self;
});
// Obstacle Class (could be rival car or static obstacle)
var Obstacle = Container.expand(function () {
var self = Container.call(this);
// Randomly choose between rival car (blue) or obstacle (gray)
var isRival = Math.random() < 0.6;
var assetId = isRival ? 'rivalCar' : 'obstacleBlock';
var obsAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Set size
if (isRival) {
obsAsset.width = 180;
obsAsset.height = 320;
} else {
obsAsset.width = 200;
obsAsset.height = 100;
}
// For collision detection
self.getBounds = function () {
return {
x: self.x - obsAsset.width / 2,
y: self.y - obsAsset.height / 2,
width: obsAsset.width,
height: obsAsset.height
};
};
// Speed (pixels per frame)
self.speed = 22 + Math.random() * 8;
// Update position
self.update = function () {
self.y += self.speed;
};
return self;
});
// Player F1 Car Class
var PlayerCar = Container.expand(function () {
var self = Container.call(this);
// Attach F1 car asset (red box for MVP)
var carAsset = self.attachAsset('playerCar', {
anchorX: 0.5,
anchorY: 0.5
});
// Set car size for MVP
carAsset.width = 180;
carAsset.height = 320;
// For collision detection
self.getBounds = function () {
return {
x: self.x - carAsset.width / 2,
y: self.y - carAsset.height / 2,
width: carAsset.width,
height: carAsset.height
};
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// --- Game Variables ---
// Tween plugin for smooth animations (e.g. car crash, obstacle movement)
// --- Asset Initialization (MVP shapes) ---
var bestScore = storage.bestScore || 0;
var trackWidth = 1200;
var trackLeft = (2048 - trackWidth) / 2;
var trackRight = trackLeft + trackWidth;
var playerStartY = 2732 - 500;
var playerCar;
var obstacles = [];
var checkpoints = [];
var dragNode = null;
var lastScore = 0;
var distance = 0;
var checkpointSpeed = 24; // Same as obstacle speed for MVP
var lastCheckpointY = 0;
var checkpointInterval = 900; // px between checkpoints
var gameOver = false;
// --- Score Display ---
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFF700
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Distance Display ---
var distTxt = new Text2('0 m', {
size: 60,
fill: 0xFFFFFF
});
distTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(distTxt);
distTxt.y = 120;
// --- Best Score Display ---
var bestScoreTxt = new Text2('Best: ' + bestScore, {
size: 60,
fill: 0x00FFAA
});
bestScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(bestScoreTxt);
bestScoreTxt.y = 200;
// --- Initialize Player Car ---
playerCar = new PlayerCar();
playerCar.x = 2048 / 2;
playerCar.y = playerStartY;
game.addChild(playerCar);
// --- Countdown State ---
var countdownActive = true;
var countdownValue = 3;
var countdownTxt = new Text2(countdownValue + '', {
size: 300,
fill: 0xFFFFFF,
font: "Impact"
});
countdownTxt.anchor.set(0.5, 0.5);
countdownTxt.x = 2048 / 2;
countdownTxt.y = 2732 / 2;
LK.gui.center.addChild(countdownTxt);
function startCountdown() {
countdownActive = true;
countdownValue = 3;
countdownTxt.setText(countdownValue + '');
countdownTxt.visible = true;
var countdownTimer = LK.setInterval(function () {
countdownValue--;
if (countdownValue > 0) {
countdownTxt.setText(countdownValue + '');
} else if (countdownValue === 0) {
countdownTxt.setText("GO!");
} else {
LK.clearInterval(countdownTimer);
countdownTxt.visible = false;
countdownActive = false;
}
}, 1000);
}
startCountdown();
// --- Touch/Drag Controls ---
function clamp(val, min, max) {
return val < min ? min : val > max ? max : val;
}
function handleMove(x, y, obj) {
if (dragNode && !gameOver && !countdownActive) {
// Clamp to track
var carWidth = playerCar.getBounds().width;
var minX = trackLeft + carWidth / 2;
var maxX = trackRight - carWidth / 2;
dragNode.x = clamp(x, minX, maxX);
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
if (countdownActive) return;
// Only allow drag if touch is on/near the car
var bounds = playerCar.getBounds();
if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) {
dragNode = playerCar;
handleMove(x, y, obj);
}
};
game.up = function (x, y, obj) {
dragNode = null;
};
// --- Obstacle Spawning ---
function spawnObstacle() {
var obs = new Obstacle();
// Random X within track, avoid spawning too close to player
var obsWidth = obs.getBounds().width;
var minX = trackLeft + obsWidth / 2;
var maxX = trackRight - obsWidth / 2;
obs.x = minX + Math.random() * (maxX - minX);
obs.y = -200;
obstacles.push(obs);
game.addChild(obs);
}
// --- Checkpoint Spawning ---
function spawnCheckpoint() {
var cp = new Checkpoint();
cp.x = 2048 / 2;
cp.y = -50;
checkpoints.push(cp);
game.addChild(cp);
}
// --- Collision Detection ---
function rectsIntersect(a, b) {
return !(a.x + a.width < b.x || a.x > b.x + b.width || a.y + a.height < b.y || a.y > b.y + b.height);
}
// --- Game Update Loop ---
game.update = function () {
if (gameOver || countdownActive) return;
// --- Move Obstacles ---
for (var i = obstacles.length - 1; i >= 0; i--) {
var obs = obstacles[i];
obs.update();
// Remove if off screen
if (obs.y - obs.getBounds().height / 2 > 2732 + 100) {
obs.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with player
if (rectsIntersect(playerCar.getBounds(), obs.getBounds())) {
// Crash!
gameOver = true;
// Update best score if needed
if (LK.getScore() > bestScore) {
bestScore = LK.getScore();
storage.bestScore = bestScore;
bestScoreTxt.setText('Best: ' + bestScore);
}
tween(playerCar, {
alpha: 0
}, {
duration: 400,
easing: tween.easeOut
});
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
// Wait for game over popup to close, then reset game state
LK.setTimeout(function () {
resetGameState();
}, 1200);
return;
}
}
// --- Move Checkpoints ---
for (var j = checkpoints.length - 1; j >= 0; j--) {
var cp = checkpoints[j];
cp.update();
// Remove if off screen
if (cp.y - cp.getBounds().height / 2 > 2732 + 50) {
cp.destroy();
checkpoints.splice(j, 1);
continue;
}
// Passed checkpoint (player crosses line)
if (!cp.passed && playerCar.y < cp.y + cp.getBounds().height / 2) {
cp.passed = true;
LK.setScore(LK.getScore() + 10);
scoreTxt.setText(LK.getScore());
}
}
// --- Distance Scoring ---
distance += checkpointSpeed;
if (distance % 100 < checkpointSpeed) {
distTxt.setText(Math.floor(distance / 10) + " m");
}
// --- Spawn Obstacles ---
if (LK.ticks % 30 === 0) {
// 1/2 chance to spawn per 0.5s
if (Math.random() < 0.5) {
spawnObstacle();
}
}
// --- Spawn Checkpoints ---
if (distance - lastCheckpointY > checkpointInterval) {
spawnCheckpoint();
lastCheckpointY = distance;
}
};
// --- Initial Checkpoint ---
spawnCheckpoint();
lastCheckpointY = 0;
// --- Track Borders (for MVP, just visual lines) ---
var leftBorder = LK.getAsset('trackBorder', {
anchorX: 0.5,
anchorY: 0,
width: 16,
height: 2732,
x: trackLeft,
y: 0
});
var rightBorder = LK.getAsset('trackBorder', {
anchorX: 0.5,
anchorY: 0,
width: 16,
height: 2732,
x: trackRight,
y: 0
});
game.addChild(leftBorder);
game.addChild(rightBorder);
// --- Center Lane Markings (dashed, MVP: just a few static lines) ---
for (var i = 0; i < 10; i++) {
var laneMark = LK.getAsset('laneMark', {
anchorX: 0.5,
anchorY: 0.5,
width: 30,
height: 180,
x: 2048 / 2,
y: i * 300 + 100
});
game.addChild(laneMark);
}
// --- Reset State on Game Over ---
function resetGameState() {
gameOver = false;
distance = 0;
LK.setScore(0);
scoreTxt.setText('0');
distTxt.setText('0 m');
bestScore = storage.bestScore || 0;
bestScoreTxt.setText('Best: ' + bestScore);
obstacles = [];
checkpoints = [];
lastCheckpointY = 0;
// Remove all obstacles and checkpoints from the stage
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child !== playerCar && child !== leftBorder && child !== rightBorder) {
child.destroy && child.destroy();
game.removeChild(child);
}
}
// Reset player car position and alpha
playerCar.x = 2048 / 2;
playerCar.y = playerStartY;
playerCar.alpha = 1;
// Restart countdown
if (countdownTxt) {
startCountdown();
}
// Initial checkpoint
spawnCheckpoint();
lastCheckpointY = 0;
}
resetGameState(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Checkpoint Class (for scoring)
var Checkpoint = Container.expand(function () {
var self = Container.call(this);
var cpAsset = self.attachAsset('checkpointLine', {
anchorX: 0.5,
anchorY: 0.5
});
cpAsset.width = 1200;
cpAsset.height = 30;
self.getBounds = function () {
return {
x: self.x - cpAsset.width / 2,
y: self.y - cpAsset.height / 2,
width: cpAsset.width,
height: cpAsset.height
};
};
self.update = function () {
self.y += checkpointSpeed;
};
return self;
});
// Obstacle Class (could be rival car or static obstacle)
var Obstacle = Container.expand(function () {
var self = Container.call(this);
// Randomly choose between rival car (blue) or obstacle (gray)
var isRival = Math.random() < 0.6;
var assetId = isRival ? 'rivalCar' : 'obstacleBlock';
var obsAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Set size
if (isRival) {
obsAsset.width = 180;
obsAsset.height = 320;
} else {
obsAsset.width = 200;
obsAsset.height = 100;
}
// For collision detection
self.getBounds = function () {
return {
x: self.x - obsAsset.width / 2,
y: self.y - obsAsset.height / 2,
width: obsAsset.width,
height: obsAsset.height
};
};
// Speed (pixels per frame)
self.speed = 22 + Math.random() * 8;
// Update position
self.update = function () {
self.y += self.speed;
};
return self;
});
// Player F1 Car Class
var PlayerCar = Container.expand(function () {
var self = Container.call(this);
// Attach F1 car asset (red box for MVP)
var carAsset = self.attachAsset('playerCar', {
anchorX: 0.5,
anchorY: 0.5
});
// Set car size for MVP
carAsset.width = 180;
carAsset.height = 320;
// For collision detection
self.getBounds = function () {
return {
x: self.x - carAsset.width / 2,
y: self.y - carAsset.height / 2,
width: carAsset.width,
height: carAsset.height
};
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// --- Game Variables ---
// Tween plugin for smooth animations (e.g. car crash, obstacle movement)
// --- Asset Initialization (MVP shapes) ---
var bestScore = storage.bestScore || 0;
var trackWidth = 1200;
var trackLeft = (2048 - trackWidth) / 2;
var trackRight = trackLeft + trackWidth;
var playerStartY = 2732 - 500;
var playerCar;
var obstacles = [];
var checkpoints = [];
var dragNode = null;
var lastScore = 0;
var distance = 0;
var checkpointSpeed = 24; // Same as obstacle speed for MVP
var lastCheckpointY = 0;
var checkpointInterval = 900; // px between checkpoints
var gameOver = false;
// --- Score Display ---
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFF700
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Distance Display ---
var distTxt = new Text2('0 m', {
size: 60,
fill: 0xFFFFFF
});
distTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(distTxt);
distTxt.y = 120;
// --- Best Score Display ---
var bestScoreTxt = new Text2('Best: ' + bestScore, {
size: 60,
fill: 0x00FFAA
});
bestScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(bestScoreTxt);
bestScoreTxt.y = 200;
// --- Initialize Player Car ---
playerCar = new PlayerCar();
playerCar.x = 2048 / 2;
playerCar.y = playerStartY;
game.addChild(playerCar);
// --- Countdown State ---
var countdownActive = true;
var countdownValue = 3;
var countdownTxt = new Text2(countdownValue + '', {
size: 300,
fill: 0xFFFFFF,
font: "Impact"
});
countdownTxt.anchor.set(0.5, 0.5);
countdownTxt.x = 2048 / 2;
countdownTxt.y = 2732 / 2;
LK.gui.center.addChild(countdownTxt);
function startCountdown() {
countdownActive = true;
countdownValue = 3;
countdownTxt.setText(countdownValue + '');
countdownTxt.visible = true;
var countdownTimer = LK.setInterval(function () {
countdownValue--;
if (countdownValue > 0) {
countdownTxt.setText(countdownValue + '');
} else if (countdownValue === 0) {
countdownTxt.setText("GO!");
} else {
LK.clearInterval(countdownTimer);
countdownTxt.visible = false;
countdownActive = false;
}
}, 1000);
}
startCountdown();
// --- Touch/Drag Controls ---
function clamp(val, min, max) {
return val < min ? min : val > max ? max : val;
}
function handleMove(x, y, obj) {
if (dragNode && !gameOver && !countdownActive) {
// Clamp to track
var carWidth = playerCar.getBounds().width;
var minX = trackLeft + carWidth / 2;
var maxX = trackRight - carWidth / 2;
dragNode.x = clamp(x, minX, maxX);
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
if (countdownActive) return;
// Only allow drag if touch is on/near the car
var bounds = playerCar.getBounds();
if (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height) {
dragNode = playerCar;
handleMove(x, y, obj);
}
};
game.up = function (x, y, obj) {
dragNode = null;
};
// --- Obstacle Spawning ---
function spawnObstacle() {
var obs = new Obstacle();
// Random X within track, avoid spawning too close to player
var obsWidth = obs.getBounds().width;
var minX = trackLeft + obsWidth / 2;
var maxX = trackRight - obsWidth / 2;
obs.x = minX + Math.random() * (maxX - minX);
obs.y = -200;
obstacles.push(obs);
game.addChild(obs);
}
// --- Checkpoint Spawning ---
function spawnCheckpoint() {
var cp = new Checkpoint();
cp.x = 2048 / 2;
cp.y = -50;
checkpoints.push(cp);
game.addChild(cp);
}
// --- Collision Detection ---
function rectsIntersect(a, b) {
return !(a.x + a.width < b.x || a.x > b.x + b.width || a.y + a.height < b.y || a.y > b.y + b.height);
}
// --- Game Update Loop ---
game.update = function () {
if (gameOver || countdownActive) return;
// --- Move Obstacles ---
for (var i = obstacles.length - 1; i >= 0; i--) {
var obs = obstacles[i];
obs.update();
// Remove if off screen
if (obs.y - obs.getBounds().height / 2 > 2732 + 100) {
obs.destroy();
obstacles.splice(i, 1);
continue;
}
// Collision with player
if (rectsIntersect(playerCar.getBounds(), obs.getBounds())) {
// Crash!
gameOver = true;
// Update best score if needed
if (LK.getScore() > bestScore) {
bestScore = LK.getScore();
storage.bestScore = bestScore;
bestScoreTxt.setText('Best: ' + bestScore);
}
tween(playerCar, {
alpha: 0
}, {
duration: 400,
easing: tween.easeOut
});
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
// Wait for game over popup to close, then reset game state
LK.setTimeout(function () {
resetGameState();
}, 1200);
return;
}
}
// --- Move Checkpoints ---
for (var j = checkpoints.length - 1; j >= 0; j--) {
var cp = checkpoints[j];
cp.update();
// Remove if off screen
if (cp.y - cp.getBounds().height / 2 > 2732 + 50) {
cp.destroy();
checkpoints.splice(j, 1);
continue;
}
// Passed checkpoint (player crosses line)
if (!cp.passed && playerCar.y < cp.y + cp.getBounds().height / 2) {
cp.passed = true;
LK.setScore(LK.getScore() + 10);
scoreTxt.setText(LK.getScore());
}
}
// --- Distance Scoring ---
distance += checkpointSpeed;
if (distance % 100 < checkpointSpeed) {
distTxt.setText(Math.floor(distance / 10) + " m");
}
// --- Spawn Obstacles ---
if (LK.ticks % 30 === 0) {
// 1/2 chance to spawn per 0.5s
if (Math.random() < 0.5) {
spawnObstacle();
}
}
// --- Spawn Checkpoints ---
if (distance - lastCheckpointY > checkpointInterval) {
spawnCheckpoint();
lastCheckpointY = distance;
}
};
// --- Initial Checkpoint ---
spawnCheckpoint();
lastCheckpointY = 0;
// --- Track Borders (for MVP, just visual lines) ---
var leftBorder = LK.getAsset('trackBorder', {
anchorX: 0.5,
anchorY: 0,
width: 16,
height: 2732,
x: trackLeft,
y: 0
});
var rightBorder = LK.getAsset('trackBorder', {
anchorX: 0.5,
anchorY: 0,
width: 16,
height: 2732,
x: trackRight,
y: 0
});
game.addChild(leftBorder);
game.addChild(rightBorder);
// --- Center Lane Markings (dashed, MVP: just a few static lines) ---
for (var i = 0; i < 10; i++) {
var laneMark = LK.getAsset('laneMark', {
anchorX: 0.5,
anchorY: 0.5,
width: 30,
height: 180,
x: 2048 / 2,
y: i * 300 + 100
});
game.addChild(laneMark);
}
// --- Reset State on Game Over ---
function resetGameState() {
gameOver = false;
distance = 0;
LK.setScore(0);
scoreTxt.setText('0');
distTxt.setText('0 m');
bestScore = storage.bestScore || 0;
bestScoreTxt.setText('Best: ' + bestScore);
obstacles = [];
checkpoints = [];
lastCheckpointY = 0;
// Remove all obstacles and checkpoints from the stage
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
if (child !== playerCar && child !== leftBorder && child !== rightBorder) {
child.destroy && child.destroy();
game.removeChild(child);
}
}
// Reset player car position and alpha
playerCar.x = 2048 / 2;
playerCar.y = playerStartY;
playerCar.alpha = 1;
// Restart countdown
if (countdownTxt) {
startCountdown();
}
// Initial checkpoint
spawnCheckpoint();
lastCheckpointY = 0;
}
resetGameState();