/****
* Plugins
****/
var storage = LK.import("@upit/storage.v1", {
playerCash: 5000,
playerTier: 0,
tierXP: 0,
winStreak: 0,
bestTime: 0,
raceCount: 0,
crateCount: 0,
carColor: 14427686,
ownedColors: [14427686],
engineLevel: 0,
turboLevel: 0,
transmissionLevel: 0,
suspensionLevel: 0,
weightLevel: 0,
tireLevel: 0,
challengeRaceCount: 0,
challengeWins: 0,
challengePerfectLaunches: 0,
currentEventId: "street_sprint",
ladderStage: 0
});
/****
* Initialize Game
****/
/****
* Game
****/
var game = new LK.Game({
backgroundColor: 0x0b1020
});
/****
* Game Code
****/
/****
* Globals
****/
var crateRewardPool = [{
id: 'cash_small',
type: 'cash',
amount: 300,
weight: 28,
rarity: 'common',
label: '$300 Cash'
}, {
id: 'cash_mid',
type: 'cash',
amount: 700,
weight: 14,
rarity: 'uncommon',
label: '$700 Cash'
}, {
id: 'cash_big',
type: 'cash',
amount: 1400,
weight: 5,
rarity: 'rare',
label: '$1400 Cash'
}, {
id: 'engine_up',
type: 'upgrade',
key: 'engine',
levels: 1,
weight: 10,
rarity: 'rare',
label: 'Engine +1'
}, {
id: 'turbo_up',
type: 'upgrade',
key: 'turbo',
levels: 1,
weight: 9,
rarity: 'rare',
label: 'Turbo +1'
}, {
id: 'trans_up',
type: 'upgrade',
key: 'transmission',
levels: 1,
weight: 10,
rarity: 'uncommon',
label: 'Transmission +1'
}, {
id: 'susp_up',
type: 'upgrade',
key: 'suspension',
levels: 1,
weight: 10,
rarity: 'uncommon',
label: 'Suspension +1'
}, {
id: 'weight_up',
type: 'upgrade',
key: 'weight',
levels: 1,
weight: 8,
rarity: 'rare',
label: 'Weight Reduction +1'
}, {
id: 'tire_up',
type: 'upgrade',
key: 'tire',
levels: 1,
weight: 11,
rarity: 'uncommon',
label: 'Tires +1'
}, {
id: 'paint_unlock',
type: 'cosmetic',
reward: 'paint',
weight: 10,
rarity: 'epic',
label: 'Random Paint'
}, {
id: 'bonus_crate',
type: 'crate',
amount: 1,
weight: 2,
rarity: 'legendary',
label: '+1 Bonus Crate'
}];
var crateSpinRoot = null;
var crateSpinStrip = null;
var crateSpinCards = [];
var crateSpinData = [];
var crateSpinVelocity = 0;
var crateSpinTargetX = 0;
var crateSpinning = false;
var crateSpinFinished = false;
var crateWinningReward = null;
var cratePendingReward = null;
var crateResultTitle = null;
var crateResultSubtitle = null;
var crateClaimButton = null;
var crateBackButton = null;
var WIDTH = 2048;
var HEIGHT = 2732;
var scene = null;
var currentState = 'garage';
var playerCar = null;
var opponentCar = null;
var launchLights = [];
var rpmText = null;
var speedText = null;
var gearText = null;
var distanceText = null;
var promptText = null;
var eventInfoText = null;
var raceTimer = 0;
var playerFinishTime = 0;
var opponentFinishTime = 0;
var countdownIndex = 0;
var countdownElapsed = 0;
var raceStarted = false;
var raceFinished = false;
var playerCanShift = false;
var playerLaunched = false;
var playerSpeed = 0;
var opponentSpeed = 0;
var playerDistance = 0;
var opponentDistance = 0;
var playerGear = 1;
var opponentGear = 1;
var playerRPM = 1000;
var launchAccuracy = 0;
var launchQuality = 'MISS';
var playerShiftWindowMin = 5200;
var playerShiftWindowMax = 6600;
var shiftScore = 0;
var missedShifts = 0;
var currentRaceDistance = 3000;
var currentOpponentBonus = 0;
var lastRaceSummary = null;
var foulStart = false;
var playerGearMax = 6;
var opponentGearMax = 6;
var shiftQualityCounts = {
perfect: 0,
good: 0,
late: 0,
early: 0,
bad: 0
};
var gearRatios = [0, 2.8, 2.1, 1.6, 1.3, 1.05, 0.88];
var opponentGearRatios = [0, 2.7, 2.05, 1.58, 1.28, 1.02, 0.86];
var upgradeData = [{
key: 'engine',
label: 'Engine',
baseCost: 1000,
maxLevel: 5
}, {
key: 'turbo',
label: 'Turbo',
baseCost: 1400,
maxLevel: 5
}, {
key: 'transmission',
label: 'Transmission',
baseCost: 900,
maxLevel: 5
}, {
key: 'suspension',
label: 'Suspension',
baseCost: 700,
maxLevel: 5
}, {
key: 'weight',
label: 'Weight Reduction',
baseCost: 1100,
maxLevel: 5
}, {
key: 'tire',
label: 'Tires',
baseCost: 600,
maxLevel: 5
}];
var eventDefinitions = [{
id: 'street_sprint',
name: 'Street Sprint',
type: 'normal',
baseReward: 900,
xpReward: 40,
distance: 3000,
tierRequired: 0,
launchMultiplier: 1.0,
shiftMultiplier: 1.0,
mistakeLimit: 99,
description: 'Balanced standard drag event.'
}, {
id: 'perfect_shift',
name: 'Perfect Shift Challenge',
type: 'shift',
baseReward: 1100,
xpReward: 50,
distance: 3200,
tierRequired: 1,
launchMultiplier: 0.9,
shiftMultiplier: 1.8,
mistakeLimit: 2,
description: 'Big rewards for clean shifts.'
}, {
id: 'reaction_test',
name: 'Reaction Test',
type: 'launch',
baseReward: 1200,
xpReward: 55,
distance: 2800,
tierRequired: 2,
launchMultiplier: 2.0,
shiftMultiplier: 0.8,
mistakeLimit: 99,
description: 'Launch quality matters most.'
}, {
id: 'rival_ladder',
name: 'Rival Ladder',
type: 'ladder',
baseReward: 1800,
xpReward: 80,
distance: 3000,
tierRequired: 3,
launchMultiplier: 1.2,
shiftMultiplier: 1.2,
mistakeLimit: 3,
description: 'Beat a sequence of rising rivals.'
}];
var colorPool = [0xdc2626, 0x2563eb, 0x22c55e, 0xf59e0b, 0xa855f7, 0xec4899, 0x14b8a6, 0xf97316];
/****
* Helpers
****/
function clearScene() {
if (scene) {
scene.destroy();
scene = null;
}
game.update = function () {};
game.down = function () {};
}
function formatCash(v) {
return '$' + Math.floor(v);
}
function getUpgradeLevel(key) {
return storage[key + 'Level'] || 0;
}
function getUpgradeCost(key, baseCost) {
var level = getUpgradeLevel(key);
return baseCost + level * Math.floor(baseCost * 0.65);
}
function getPlayerStats() {
var engine = getUpgradeLevel('engine');
var turbo = getUpgradeLevel('turbo');
var transmission = getUpgradeLevel('transmission');
var suspension = getUpgradeLevel('suspension');
var weight = getUpgradeLevel('weight');
var tire = getUpgradeLevel('tire');
return {
hp: 160 + engine * 28 + turbo * 36,
torque: 210 + engine * 22 + turbo * 30,
grip: 72 + tire * 6 + suspension * 4,
mass: Math.max(980, 1450 - weight * 55),
shiftSpeed: 1 + transmission * 0.08,
traction: 1 + tire * 0.06 + suspension * 0.03
};
}
function getOpponentStats() {
var tier = storage.playerTier || 0;
return {
hp: 170 + tier * 35 + currentOpponentBonus,
torque: 220 + tier * 28 + currentOpponentBonus * 0.75,
grip: 75 + tier * 5,
mass: 1425 - tier * 35,
shiftSpeed: 1.03 + tier * 0.05,
traction: 1.02 + tier * 0.05
};
}
function makeLabel(parent, txt, x, y, size, color, ax, ay) {
var t = parent.addChild(new Text2(txt, {
size: size || 32,
fill: color || '#FFFFFF'
}));
t.anchor.set(ax == null ? 0.5 : ax, ay == null ? 0.5 : ay);
t.x = x;
t.y = y;
return t;
}
function createCar(isOpponent) {
var c = new Container();
var body = c.addChild(LK.getAsset(isOpponent ? 'opponentBody' : 'carBody', {
anchorX: 0.5,
anchorY: 0.5
}));
var windowObj = c.addChild(LK.getAsset('carWindow', {
anchorX: 0.5,
anchorY: 0.5
}));
windowObj.y = -10;
var wheels = [];
var rims = [];
for (var i = 0; i < 4; i++) {
wheels.push(c.addChild(LK.getAsset('wheel', {
anchorX: 0.5,
anchorY: 0.5
})));
rims.push(c.addChild(LK.getAsset('rim', {
anchorX: 0.5,
anchorY: 0.5
})));
}
var positions = [{
x: -45,
y: -36
}, {
x: 45,
y: -36
}, {
x: -45,
y: 36
}, {
x: 45,
y: 36
}];
for (var j = 0; j < 4; j++) {
wheels[j].x = positions[j].x;
wheels[j].y = positions[j].y;
rims[j].x = positions[j].x;
rims[j].y = positions[j].y;
}
c.setColor = function (color) {
body.tint = color;
};
c.spinWheels = function (speed) {
for (var k = 0; k < rims.length; k++) {
rims[k].rotation += speed;
}
};
return c;
}
function createProgressBar(parent, x, y, width, ratio, fillColor) {
var bar = new Container();
parent.addChild(bar);
bar.x = x;
bar.y = y;
var bg = bar.addChild(LK.getAsset('progressBg', {
anchorX: 0,
anchorY: 0.5
}));
bg.width = width;
var fill = bar.addChild(LK.getAsset('progressFill', {
anchorX: 0,
anchorY: 0.5
}));
fill.width = Math.max(0, Math.min(width, width * ratio));
fill.tint = fillColor || 0x22c55e;
return {
root: bar,
bg: bg,
fill: fill
};
}
function resetRaceValues() {
raceTimer = 0;
playerFinishTime = 0;
opponentFinishTime = 0;
countdownIndex = 0;
countdownElapsed = 0;
raceStarted = false;
raceFinished = false;
playerCanShift = false;
playerLaunched = false;
playerSpeed = 0;
opponentSpeed = 0;
playerDistance = 0;
opponentDistance = 0;
playerGear = 1;
opponentGear = 1;
playerRPM = 1000;
launchAccuracy = 0;
launchQuality = 'MISS';
shiftScore = 0;
missedShifts = 0;
currentOpponentBonus = 0;
foulStart = false;
shiftQualityCounts = {
perfect: 0,
good: 0,
late: 0,
early: 0,
bad: 0
};
if (opponentCar) opponentCar.aiRPM = 3200;
}
function getXPNeededForNextTier() {
return 100 + (storage.playerTier || 0) * 40;
}
function addTierXP(amount) {
storage.tierXP = (storage.tierXP || 0) + amount;
var leveledUp = false;
while (storage.tierXP >= getXPNeededForNextTier()) {
storage.tierXP -= getXPNeededForNextTier();
storage.playerTier = (storage.playerTier || 0) + 1;
storage.crateCount = (storage.crateCount || 0) + 1;
leveledUp = true;
}
return leveledUp;
}
function getChallengeSet() {
return [{
id: 'race3',
label: 'Run 3 races',
progress: storage.challengeRaceCount || 0,
target: 3,
reward: 350
}, {
id: 'win2',
label: 'Win 2 races',
progress: storage.challengeWins || 0,
target: 2,
reward: 500
}, {
id: 'perfect1',
label: 'Hit 1 perfect launch',
progress: storage.challengePerfectLaunches || 0,
target: 1,
reward: 450
}];
}
function applyChallengeRewards() {
var rewards = [];
var set = getChallengeSet();
for (var i = 0; i < set.length; i++) {
var c = set[i];
if (c.progress >= c.target) {
storage.playerCash += c.reward;
rewards.push(c.label + ' +' + formatCash(c.reward));
}
}
if ((storage.challengeRaceCount || 0) >= 3) storage.challengeRaceCount = 0;
if ((storage.challengeWins || 0) >= 2) storage.challengeWins = 0;
if ((storage.challengePerfectLaunches || 0) >= 1) storage.challengePerfectLaunches = 0;
return rewards;
}
function unlockRandomCosmetic() {
var owned = storage.ownedColors || [0xdc2626];
var available = [];
for (var i = 0; i < colorPool.length; i++) {
var found = false;
for (var j = 0; j < owned.length; j++) {
if (owned[j] === colorPool[i]) {
found = true;
break;
}
}
if (!found) available.push(colorPool[i]);
}
if (available.length <= 0) {
storage.playerCash += 300;
return {
type: 'cash',
value: 300
};
}
var color = available[Math.floor(Math.random() * available.length)];
owned.push(color);
storage.ownedColors = owned;
storage.carColor = color;
return {
type: 'color',
value: color
};
}
function getCurrentEvent() {
for (var i = 0; i < eventDefinitions.length; i++) {
if (eventDefinitions[i].id === storage.currentEventId) return eventDefinitions[i];
}
return eventDefinitions[0];
}
function isEventUnlocked(eventDef) {
return (storage.playerTier || 0) >= eventDef.tierRequired;
}
function getNextLockedEvent() {
for (var i = 0; i < eventDefinitions.length; i++) {
if (!isEventUnlocked(eventDefinitions[i])) return eventDefinitions[i];
}
return null;
}
function getRewardCardAssetByRarity(rarity) {
if (rarity === 'legendary') return 'rewardCardLegendary';
if (rarity === 'epic') return 'rewardCardEpic';
if (rarity === 'rare') return 'rewardCardRare';
if (rarity === 'uncommon') return 'rewardCardUncommon';
return 'rewardCardCommon';
}
function cloneRewardEntry(entry) {
return {
id: entry.id,
type: entry.type,
amount: entry.amount,
key: entry.key,
levels: entry.levels,
weight: entry.weight,
rarity: entry.rarity,
reward: entry.reward,
label: entry.label
};
}
function weightedRewardRoll(pool) {
var totalWeight = 0;
for (var i = 0; i < pool.length; i++) totalWeight += pool[i].weight;
var roll = Math.random() * totalWeight;
var running = 0;
for (var j = 0; j < pool.length; j++) {
running += pool[j].weight;
if (roll <= running) return cloneRewardEntry(pool[j]);
}
return cloneRewardEntry(pool[pool.length - 1]);
}
function getUpgradeMaxLevel(key) {
for (var i = 0; i < upgradeData.length; i++) {
if (upgradeData[i].key === key) return upgradeData[i].maxLevel;
}
return 5;
}
function getRandomUnownedColor() {
var owned = storage.ownedColors || [0xdc2626];
var available = [];
for (var i = 0; i < colorPool.length; i++) {
var found = false;
for (var j = 0; j < owned.length; j++) {
if (owned[j] === colorPool[i]) {
found = true;
break;
}
}
if (!found) available.push(colorPool[i]);
}
if (available.length <= 0) return null;
return available[Math.floor(Math.random() * available.length)];
}
function sanitizeCrateReward(reward) {
var result = cloneRewardEntry(reward);
if (result.type === 'upgrade') {
var current = getUpgradeLevel(result.key);
var maxLevel = getUpgradeMaxLevel(result.key);
if (current >= maxLevel) {
return {
id: 'fallback_cash_' + result.key,
type: 'cash',
amount: 900,
rarity: 'uncommon',
label: 'Upgrade Maxed → $900'
};
}
}
if (result.type === 'cosmetic' && result.reward === 'paint') {
var color = getRandomUnownedColor();
if (color == null) {
return {
id: 'fallback_cash_paint',
type: 'cash',
amount: 600,
rarity: 'uncommon',
label: 'Duplicate Paint → $600'
};
}
result.colorValue = color;
result.label = 'New Paint';
}
return result;
}
function applyCrateReward(reward) {
if (!reward) {
return {
title: 'No Reward',
subtitle: 'Nothing was applied.'
};
}
if (reward.type === 'cash') {
storage.playerCash = (storage.playerCash || 0) + (reward.amount || 0);
return {
title: reward.label,
subtitle: 'Cash added to your garage funds.'
};
}
if (reward.type === 'crate') {
storage.crateCount = (storage.crateCount || 0) + (reward.amount || 1);
return {
title: reward.label,
subtitle: 'A bonus crate was added.'
};
}
if (reward.type === 'upgrade') {
var current = getUpgradeLevel(reward.key);
var maxLevel = getUpgradeMaxLevel(reward.key);
var levelsToAdd = reward.levels || 1;
var nextLevel = Math.min(maxLevel, current + levelsToAdd);
storage[reward.key + 'Level'] = nextLevel;
return {
title: reward.label,
subtitle: reward.key + ' upgraded to level ' + nextLevel + '.'
};
}
if (reward.type === 'cosmetic' && reward.reward === 'paint') {
var owned = storage.ownedColors || [0xdc2626];
owned.push(reward.colorValue);
storage.ownedColors = owned;
storage.carColor = reward.colorValue;
return {
title: reward.label,
subtitle: 'New paint unlocked and equipped.'
};
}
return {
title: reward.label || 'Reward',
subtitle: 'Reward claimed.'
};
}
function getRandomPoolRewardForVisual() {
return cloneRewardEntry(crateRewardPool[Math.floor(Math.random() * crateRewardPool.length)]);
}
function buildSpinVisualRewards(winningReward) {
var items = [];
for (var i = 0; i < 36; i++) items.push(getRandomPoolRewardForVisual());
var winIndex = 29;
items[winIndex] = cloneRewardEntry(winningReward);
return {
items: items,
winIndex: winIndex
};
}
function makeRewardCard(parent, reward, x, y) {
var assetName = getRewardCardAssetByRarity(reward.rarity);
var root = parent.addChild(LK.getAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
}));
root.x = x;
root.y = y;
makeLabel(parent, reward.label || 'Reward', x, y - 22, 24, '#FFFFFF');
makeLabel(parent, (reward.rarity || 'common').toUpperCase(), x, y + 24, 18, '#e5e7eb');
return root;
}
/****
* Garage
****/
function initGarage() {
clearScene();
currentState = 'garage';
LK.playMusic('garageMusic', {
loop: true
});
scene = game.addChild(new Container());
makeLabel(scene, 'MIDNIGHT STRIP', WIDTH / 2, 90, 80, '#FFFFFF');
makeLabel(scene, 'Career Events • Build • Launch • Upgrade', WIDTH / 2, 160, 30, '#93c5fd');
var cashPanel = scene.addChild(LK.getAsset('widePanel', {
anchorX: 0.5,
anchorY: 0.5
}));
cashPanel.x = WIDTH / 2;
cashPanel.y = 300;
makeLabel(scene, 'Cash: ' + formatCash(storage.playerCash || 0), WIDTH / 2 - 300, 280, 36, '#22c55e', 0, 0.5);
makeLabel(scene, 'Tier ' + ((storage.playerTier || 0) + 1), WIDTH / 2 - 300, 330, 28, '#facc15', 0, 0.5);
makeLabel(scene, 'Win Streak: ' + (storage.winStreak || 0), WIDTH / 2 + 60, 280, 28, '#FFFFFF', 0, 0.5);
makeLabel(scene, 'Crates: ' + (storage.crateCount || 0), WIDTH / 2 + 60, 330, 28, '#FFFFFF', 0, 0.5);
var xpNeeded = getXPNeededForNextTier();
createProgressBar(scene, WIDTH / 2 - 300, 380, 560, (storage.tierXP || 0) / xpNeeded, 0x3b82f6);
makeLabel(scene, 'Tier Progress: ' + (storage.tierXP || 0) + ' / ' + xpNeeded, WIDTH / 2, 425, 22, '#cbd5e1');
playerCar = scene.addChild(createCar(false));
playerCar.x = 270;
playerCar.y = 700;
playerCar.scale.set(1.65);
playerCar.setColor(storage.carColor || 0xdc2626);
var stats = getPlayerStats();
makeLabel(scene, 'HP: ' + stats.hp, 110, 900, 30, '#FFFFFF', 0, 0.5);
makeLabel(scene, 'Torque: ' + stats.torque, 110, 945, 30, '#FFFFFF', 0, 0.5);
makeLabel(scene, 'Grip: ' + stats.grip, 110, 990, 30, '#FFFFFF', 0, 0.5);
makeLabel(scene, 'Weight: ' + stats.mass + 'kg', 110, 1035, 30, '#FFFFFF', 0, 0.5);
makeLabel(scene, 'Best Time: ' + (storage.bestTime ? storage.bestTime.toFixed(2) + 's' : '--'), 110, 1080, 30, '#93c5fd', 0, 0.5);
makeLabel(scene, 'EVENTS', 1280, 520, 40, '#facc15');
for (var e = 0; e < eventDefinitions.length; e++) {
(function (eventDef, index) {
var unlocked = isEventUnlocked(eventDef);
var isSelected = storage.currentEventId === eventDef.id;
var card = scene.addChild(LK.getAsset(unlocked ? 'eventCard' : 'eventLockedCard', {
anchorX: 0.5,
anchorY: 0.5
}));
card.x = 930 + index % 2 * 470;
card.y = 650 + Math.floor(index / 2) * 150;
if (isSelected && unlocked) card.tint = 0x2563eb;
makeLabel(scene, eventDef.name, card.x, card.y - 24, 28, '#FFFFFF');
makeLabel(scene, unlocked ? 'Reward ' + formatCash(eventDef.baseReward) : 'Unlocks at Tier ' + (eventDef.tierRequired + 1), card.x, card.y + 6, 20, unlocked ? '#22c55e' : '#facc15');
makeLabel(scene, eventDef.description, card.x, card.y + 34, 18, '#cbd5e1');
card.down = function () {
if (!unlocked) return;
storage.currentEventId = eventDef.id;
initGarage();
};
})(eventDefinitions[e], e);
}
var nextLocked = getNextLockedEvent();
makeLabel(scene, nextLocked ? 'Next Event: ' + nextLocked.name + ' at Tier ' + (nextLocked.tierRequired + 1) : 'All events unlocked', 1280, 980, 24, '#93c5fd');
makeLabel(scene, 'CHALLENGES', 1280, 1070, 40, '#facc15');
var challenges = getChallengeSet();
for (var c = 0; c < challenges.length; c++) {
var challenge = challenges[c];
var card2 = scene.addChild(LK.getAsset('challengeCard', {
anchorX: 0.5,
anchorY: 0.5
}));
card2.x = 1280;
card2.y = 1180 + c * 145;
makeLabel(scene, challenge.label, card2.x - 230, card2.y - 18, 28, '#FFFFFF', 0, 0.5);
makeLabel(scene, challenge.progress + ' / ' + challenge.target, card2.x - 230, card2.y + 18, 22, '#93c5fd', 0, 0.5);
makeLabel(scene, formatCash(challenge.reward), card2.x + 180, card2.y, 22, '#22c55e');
}
makeLabel(scene, 'UPGRADES', WIDTH / 2, 1620, 40, '#facc15');
for (var i = 0; i < upgradeData.length; i++) {
(function (data, index) {
var level = getUpgradeLevel(data.key);
var cost = getUpgradeCost(data.key, data.baseCost);
var card3 = scene.addChild(LK.getAsset('upgradeCard', {
anchorX: 0.5,
anchorY: 0.5
}));
card3.x = 560 + index % 2 * 930;
card3.y = 1740 + Math.floor(index / 2) * 120;
makeLabel(scene, data.label + ' Lv.' + level, card3.x, card3.y - 18, 28, '#FFFFFF');
makeLabel(scene, level >= data.maxLevel ? 'MAXED' : 'Buy ' + formatCash(cost), card3.x, card3.y + 18, 22, level >= data.maxLevel ? '#facc15' : '#22c55e');
card3.down = function () {
buyUpgrade(data);
};
})(upgradeData[i], i);
}
var selectedEvent = getCurrentEvent();
makeLabel(scene, 'Selected Event: ' + selectedEvent.name, WIDTH / 2, 2230, 32, '#FFFFFF');
makeLabel(scene, 'Type: ' + selectedEvent.type + ' • Distance: ' + selectedEvent.distance + 'm • XP: ' + selectedEvent.xpReward, WIDTH / 2, 2280, 24, '#93c5fd');
var raceBtn = scene.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5
}));
raceBtn.x = WIDTH / 2;
raceBtn.y = 2400;
makeLabel(scene, 'START EVENT', WIDTH / 2, 2400, 38, '#08110d');
raceBtn.down = function () {
startCountdown();
};
var crateBtn = scene.addChild(LK.getAsset('buttonAlt', {
anchorX: 0.5,
anchorY: 0.5
}));
crateBtn.x = WIDTH / 2;
crateBtn.y = 2520;
makeLabel(scene, 'OPEN CRATE', WIDTH / 2, 2520, 34, '#eff6ff');
crateBtn.down = function () {
openCratePopup();
};
}
function buyUpgrade(data) {
var level = getUpgradeLevel(data.key);
if (level >= data.maxLevel) return;
var cost = getUpgradeCost(data.key, data.baseCost);
if ((storage.playerCash || 0) < cost) return;
storage.playerCash -= cost;
storage[data.key + 'Level'] = level + 1;
LK.getSound('cashReward').play();
initGarage();
}
/****
* Countdown
****/
function startCountdown() {
resetRaceValues();
clearScene();
currentState = 'countdown';
LK.playMusic('raceMusic', {
loop: true
});
var eventDef = getCurrentEvent();
currentRaceDistance = eventDef.distance;
if (eventDef.type === 'ladder') currentOpponentBonus = (storage.ladderStage || 0) * 20;
scene = game.addChild(new Container());
var track = scene.addChild(LK.getAsset('track', {
anchorX: 0.5,
anchorY: 0.5
}));
track.x = WIDTH / 2;
track.y = 1500;
var finish = scene.addChild(LK.getAsset('finishLine', {
anchorX: 0.5,
anchorY: 0.5
}));
finish.x = 1760;
finish.y = 1500;
playerCar = scene.addChild(createCar(false));
playerCar.x = 340;
playerCar.y = 1380;
playerCar.setColor(storage.carColor || 0xdc2626);
opponentCar = scene.addChild(createCar(true));
opponentCar.x = 340;
opponentCar.y = 1620;
opponentCar.aiRPM = 3200;
makeLabel(scene, eventDef.name, WIDTH / 2, 150, 54, '#FFFFFF');
makeLabel(scene, eventDef.description, WIDTH / 2, 215, 26, '#facc15');
launchLights = [];
for (var i = 0; i < 3; i++) {
var light = scene.addChild(LK.getAsset('lightOff', {
anchorX: 0.5,
anchorY: 0.5
}));
light.x = WIDTH / 2;
light.y = 520 + i * 72;
launchLights.push(light);
}
rpmText = makeLabel(scene, 'RPM: 1000', WIDTH / 2, 920, 42, '#22c55e');
promptText = makeLabel(scene, 'Tap on GREEN', WIDTH / 2, 1000, 34, '#FFFFFF');
eventInfoText = makeLabel(scene, 'Distance: ' + currentRaceDistance + 'm', WIDTH / 2, 1060, 26, '#93c5fd');
game.down = function () {
if (currentState !== 'countdown') return;
if (countdownIndex < 3) {
foulStart = true;
launchQuality = 'FOUL';
launchAccuracy = 0.15;
playerLaunched = true;
LK.getSound('launch').play();
beginRace();
return;
}
foulStart = false;
playerLaunched = true;
var rpmScore = 1 - Math.min(1, Math.abs(5000 - playerRPM) / 2200);
launchAccuracy = Math.max(0, rpmScore);
if (launchAccuracy > 0.88) launchQuality = 'PERFECT';else if (launchAccuracy > 0.68) launchQuality = 'GOOD';else if (launchAccuracy > 0.45) launchQuality = 'OK';else launchQuality = 'POOR';
LK.getSound('launch').play();
beginRace();
};
game.update = function () {
if (currentState !== 'countdown') return;
countdownElapsed += 1 / 60;
playerRPM += 85;
if (playerRPM > 7000) playerRPM = 3200;
rpmText.setText('RPM: ' + Math.floor(playerRPM));
if (countdownElapsed >= 0.75 && countdownIndex < 3) {
countdownElapsed = 0;
launchLights[countdownIndex].destroy();
launchLights[countdownIndex] = scene.addChild(LK.getAsset(countdownIndex < 2 ? 'lightRed' : 'lightGreen', {
anchorX: 0.5,
anchorY: 0.5
}));
launchLights[countdownIndex].x = WIDTH / 2;
launchLights[countdownIndex].y = 520 + countdownIndex * 72;
countdownIndex++;
}
if (countdownIndex >= 3 && !playerLaunched && countdownElapsed >= 0.40) {
foulStart = false;
launchQuality = 'LATE';
launchAccuracy = 0.2;
playerLaunched = true;
beginRace();
}
};
}
/****
* Race
****/
function beginRace() {
currentState = 'race';
raceStarted = true;
raceFinished = false;
raceTimer = 0;
playerCanShift = false;
var playerStats = getPlayerStats();
var aiStats = getOpponentStats();
var eventDef = getCurrentEvent();
speedText = makeLabel(scene, 'Speed: 0 mph', 150, 120, 34, '#22c55e', 0, 0.5);
gearText = makeLabel(scene, 'Gear: 1', 150, 170, 34, '#facc15', 0, 0.5);
distanceText = makeLabel(scene, 'Distance: 0 m', 150, 220, 34, '#FFFFFF', 0, 0.5);
promptText.setText('Tap to shift through the gears');
game.down = function () {
if (currentState !== 'race' || !raceStarted || raceFinished) return;
var shiftCenter = (playerShiftWindowMin + playerShiftWindowMax) * 0.5;
var shiftDelta = playerRPM - shiftCenter;
if (playerGear >= playerGearMax) {
promptText.setText('TOP GEAR');
return;
}
if (playerRPM < playerShiftWindowMin - 350) {
shiftQualityCounts.early++;
missedShifts++;
playerGear++;
playerRPM = 3400;
playerSpeed *= 0.96;
promptText.setText('EARLY SHIFT');
LK.getSound('shiftGear').play();
} else if (playerRPM >= playerShiftWindowMin && playerRPM <= playerShiftWindowMax) {
if (Math.abs(shiftDelta) < 160) {
shiftQualityCounts.perfect++;
shiftScore += 3;
promptText.setText('PERFECT SHIFT');
} else {
shiftQualityCounts.good++;
shiftScore += 2;
promptText.setText('GOOD SHIFT');
}
playerGear++;
playerRPM = 3900 - getUpgradeLevel('transmission') * 140;
playerSpeed *= 1.015;
LK.getSound('shiftGear').play();
} else if (playerRPM > playerShiftWindowMax && playerRPM <= playerShiftWindowMax + 550) {
shiftQualityCounts.late++;
shiftScore += 1;
missedShifts++;
playerGear++;
playerRPM = 4300;
playerSpeed *= 0.985;
promptText.setText('LATE SHIFT');
LK.getSound('shiftGear').play();
} else {
shiftQualityCounts.bad++;
missedShifts++;
playerSpeed *= 0.95;
promptText.setText('BAD SHIFT');
}
playerCanShift = false;
};
game.update = function () {
if (currentState !== 'race' || raceFinished) return;
raceTimer += 1 / 60;
var currentRatio = gearRatios[playerGear] || 0.88;
var opponentRatio = opponentGearRatios[opponentGear] || 0.86;
var playerLaunchBoost = foulStart ? 0.68 : 0.78 + launchAccuracy * 0.7;
var playerAccel = playerStats.torque * currentRatio / playerStats.mass * 1.18 * playerLaunchBoost;
var aiLaunchBoost = 0.9 + (storage.playerTier || 0) * 0.03;
var aiAccel = aiStats.torque * opponentRatio / aiStats.mass * 1.1 * aiLaunchBoost;
playerRPM += 85 + playerGear * 32 + getUpgradeLevel('engine') * 8 + playerSpeed * 0.045;
if (playerRPM >= playerShiftWindowMin && playerRPM <= playerShiftWindowMax) {
playerCanShift = true;
promptText.setText('SHIFT NOW');
}
if (playerRPM > playerShiftWindowMax + 650) {
playerRPM = 6850;
playerSpeed *= 0.992;
promptText.setText('ON LIMITER');
}
if (playerGear <= playerGearMax) {
playerSpeed += playerAccel;
}
if (!opponentCar.aiRPM) opponentCar.aiRPM = 3200;
opponentCar.aiRPM += 95 + opponentGear * 24 + opponentSpeed * 0.035;
var aiShiftThreshold = 6100 + (storage.playerTier || 0) * 70 + currentOpponentBonus * 2;
if (opponentGear < opponentGearMax && opponentCar.aiRPM >= aiShiftThreshold) {
opponentGear++;
opponentCar.aiRPM = 4100;
}
opponentSpeed += aiAccel;
playerSpeed = Math.min(playerSpeed, 60 + playerStats.hp * 0.62);
opponentSpeed = Math.min(opponentSpeed, 60 + aiStats.hp * 0.6);
playerDistance += playerSpeed * 0.17;
opponentDistance += opponentSpeed * 0.17;
playerCar.x = 340 + Math.min(1320, playerDistance / currentRaceDistance * 1320);
opponentCar.x = 340 + Math.min(1320, opponentDistance / currentRaceDistance * 1320);
playerCar.spinWheels(playerSpeed * 0.01);
opponentCar.spinWheels(opponentSpeed * 0.01);
speedText.setText('Speed: ' + Math.floor(playerSpeed) + ' mph');
gearText.setText('Gear: ' + playerGear);
distanceText.setText('Distance: ' + Math.floor(playerDistance) + ' m');
if (!playerFinishTime && playerDistance >= currentRaceDistance) playerFinishTime = raceTimer;
if (!opponentFinishTime && opponentDistance >= currentRaceDistance) opponentFinishTime = raceTimer;
if (playerFinishTime && opponentFinishTime) {
raceFinished = true;
processRaceResults();
}
};
}
/****
* Results processing
****/
function processRaceResults() {
var eventDef = getCurrentEvent();
var playerWon = playerFinishTime <= opponentFinishTime;
var raceCash = playerWon ? eventDef.baseReward : Math.floor(eventDef.baseReward * 0.35);
var launchBase = launchQuality === 'PERFECT' ? 300 : launchQuality === 'GOOD' ? 180 : launchQuality === 'OK' ? 100 : launchQuality === 'POOR' ? 40 : 0;
var launchBonus = Math.floor(launchBase * eventDef.launchMultiplier);
var shiftBonus = Math.floor(shiftScore * 70 * eventDef.shiftMultiplier);
var shiftPenalty = 0;
var eventBonus = 0;
var foulPenalty = foulStart ? 250 : 0;
if (eventDef.type === 'shift') {
shiftPenalty = missedShifts * 60;
if (missedShifts <= eventDef.mistakeLimit) eventBonus += 250;
}
if (eventDef.type === 'launch' && launchQuality === 'PERFECT') {
eventBonus += 350;
}
var streakBonus = 0;
var personalBestBonus = 0;
var challengeRewards = [];
var tierUp = false;
var crateEarned = false;
var newBest = false;
var ladderAdvanced = false;
if (playerWon) {
storage.winStreak = (storage.winStreak || 0) + 1;
storage.challengeWins = (storage.challengeWins || 0) + 1;
} else {
storage.winStreak = 0;
}
if ((storage.winStreak || 0) > 0) streakBonus = Math.min(400, (storage.winStreak || 0) * 60);
if (launchQuality === 'PERFECT') {
storage.challengePerfectLaunches = (storage.challengePerfectLaunches || 0) + 1;
}
storage.challengeRaceCount = (storage.challengeRaceCount || 0) + 1;
storage.raceCount = (storage.raceCount || 0) + 1;
if (!storage.bestTime || playerFinishTime < storage.bestTime) {
storage.bestTime = playerFinishTime;
personalBestBonus = 250;
newBest = true;
}
var xpGain = eventDef.xpReward + (playerWon ? 10 : 0);
if (launchQuality === 'PERFECT') xpGain += 10;
if (newBest) xpGain += 10;
if (eventDef.type === 'ladder') {
if (playerWon) {
storage.ladderStage = (storage.ladderStage || 0) + 1;
ladderAdvanced = true;
if ((storage.ladderStage || 0) >= 3) {
eventBonus += 500;
storage.ladderStage = 0;
}
} else {
storage.ladderStage = 0;
}
}
var oldCrates = storage.crateCount || 0;
tierUp = addTierXP(xpGain);
crateEarned = (storage.crateCount || 0) > oldCrates;
challengeRewards = applyChallengeRewards();
var challengeCash = 0;
for (var i = 0; i < challengeRewards.length; i++) {
if (challengeRewards[i].indexOf('$') >= 0) {
var amount = parseInt(challengeRewards[i].split('$')[1], 10);
if (!isNaN(amount)) challengeCash += amount;
}
}
var totalEarned = raceCash + launchBonus + shiftBonus - shiftPenalty - foulPenalty + streakBonus + personalBestBonus + eventBonus + challengeCash;
storage.playerCash = (storage.playerCash || 0) + raceCash + launchBonus + shiftBonus - shiftPenalty - foulPenalty + streakBonus + personalBestBonus + eventBonus;
lastRaceSummary = {
eventName: eventDef.name,
playerWon: playerWon,
launchQuality: launchQuality,
playerTime: playerFinishTime,
opponentTime: opponentFinishTime,
raceCash: raceCash,
launchBonus: launchBonus,
shiftBonus: shiftBonus,
shiftPenalty: shiftPenalty,
foulStart: foulStart,
foulPenalty: foulPenalty,
eventBonus: eventBonus,
streakBonus: streakBonus,
personalBestBonus: personalBestBonus,
challengeRewards: challengeRewards,
totalEarned: totalEarned,
xpGain: xpGain,
tierUp: tierUp,
crateEarned: crateEarned,
ladderAdvanced: ladderAdvanced,
shiftBreakdown: {
perfect: shiftQualityCounts.perfect,
good: shiftQualityCounts.good,
late: shiftQualityCounts.late,
early: shiftQualityCounts.early,
bad: shiftQualityCounts.bad
},
nextUnlockText: getNextLockedEvent() ? 'Reach Tier ' + (getNextLockedEvent().tierRequired + 1) + ' to unlock ' + getNextLockedEvent().name : 'All event types unlocked'
};
if (playerWon) LK.getSound('raceWin').play();else LK.getSound('raceLose').play();
showResults();
}
/****
* Results screen
****/
function showResults() {
currentState = 'results';
clearScene();
scene = game.addChild(new Container());
var s = lastRaceSummary;
makeLabel(scene, s.playerWon ? 'EVENT WON' : 'EVENT LOST', WIDTH / 2, 120, 82, s.playerWon ? '#22c55e' : '#ef4444');
makeLabel(scene, s.eventName, WIDTH / 2, 190, 30, '#93c5fd');
makeLabel(scene, 'You Earned', WIDTH / 2, 320, 38, '#facc15');
makeLabel(scene, 'Race: ' + formatCash(s.raceCash), WIDTH / 2, 390, 30, '#FFFFFF');
makeLabel(scene, 'Launch Bonus: ' + formatCash(s.launchBonus) + ' (' + s.launchQuality + ')', WIDTH / 2, 440, 30, '#FFFFFF');
makeLabel(scene, 'Shift Bonus: ' + formatCash(s.shiftBonus), WIDTH / 2, 490, 30, '#FFFFFF');
makeLabel(scene, 'Shift Penalty: -' + formatCash(s.shiftPenalty), WIDTH / 2, 540, 30, '#FFFFFF');
makeLabel(scene, 'Foul Penalty: -' + formatCash(s.foulPenalty), WIDTH / 2, 590, 30, '#FFFFFF');
makeLabel(scene, 'Event Bonus: ' + formatCash(s.eventBonus), WIDTH / 2, 640, 30, '#FFFFFF');
makeLabel(scene, 'Streak Bonus: ' + formatCash(s.streakBonus), WIDTH / 2, 690, 30, '#FFFFFF');
makeLabel(scene, 'Personal Best: ' + formatCash(s.personalBestBonus), WIDTH / 2, 740, 30, '#FFFFFF');
makeLabel(scene, 'Total: ' + formatCash(s.totalEarned), WIDTH / 2, 810, 42, '#22c55e');
makeLabel(scene, 'Performance', WIDTH / 2, 940, 38, '#facc15');
makeLabel(scene, 'Your Time: ' + s.playerTime.toFixed(2) + 's', WIDTH / 2, 1010, 30, '#FFFFFF');
makeLabel(scene, 'Opponent Time: ' + s.opponentTime.toFixed(2) + 's', WIDTH / 2, 1060, 30, '#FFFFFF');
makeLabel(scene, 'XP Gained: ' + s.xpGain, WIDTH / 2, 1110, 30, '#93c5fd');
makeLabel(scene, 'Win Streak: ' + (storage.winStreak || 0), WIDTH / 2, 1160, 30, '#FFFFFF');
makeLabel(scene, 'Perfect Shifts: ' + s.shiftBreakdown.perfect, WIDTH / 2, 1210, 28, '#93c5fd');
makeLabel(scene, 'Good / Late / Early / Bad: ' + s.shiftBreakdown.good + ' / ' + s.shiftBreakdown.late + ' / ' + s.shiftBreakdown.early + ' / ' + s.shiftBreakdown.bad, WIDTH / 2, 1255, 24, '#FFFFFF');
if (s.ladderAdvanced) {
makeLabel(scene, 'Rival Ladder Progress: ' + (storage.ladderStage || 0) + ' / 3', WIDTH / 2, 1300, 28, '#facc15');
}
makeLabel(scene, 'Challenge Rewards', WIDTH / 2, 1450, 38, '#facc15');
if (s.challengeRewards.length <= 0) {
makeLabel(scene, 'No challenge completed this event', WIDTH / 2, 1520, 28, '#cbd5e1');
} else {
for (var i = 0; i < s.challengeRewards.length; i++) {
makeLabel(scene, s.challengeRewards[i], WIDTH / 2, 1520 + i * 42, 26, '#22c55e');
}
}
makeLabel(scene, 'Next Unlock', WIDTH / 2, 1790, 38, '#facc15');
makeLabel(scene, s.nextUnlockText, WIDTH / 2, 1860, 28, '#FFFFFF');
if (s.tierUp) makeLabel(scene, 'Tier Up! A new crate was added to your garage', WIDTH / 2, 1910, 26, '#22c55e');
if (s.crateEarned) makeLabel(scene, 'Cosmetic crate earned', WIDTH / 2, 1950, 26, '#93c5fd');
var carPreview = scene.addChild(createCar(false));
carPreview.x = WIDTH / 2;
carPreview.y = 2230;
carPreview.scale.set(1.65);
carPreview.setColor(storage.carColor || 0xdc2626);
var garageBtn = scene.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5
}));
garageBtn.x = WIDTH / 2;
garageBtn.y = 2520;
makeLabel(scene, 'BACK TO GARAGE', WIDTH / 2, 2520, 36, '#08110d');
garageBtn.down = function () {
initGarage();
};
}
/***********************
* CRATE UI
***********************/
function openCratePopup() {
if ((storage.crateCount || 0) <= 0) return;
storage.crateCount -= 1;
clearScene();
currentState = 'crate_opening';
scene = game.addChild(new Container());
var overlay = scene.addChild(LK.getAsset('overlayBg', {
anchorX: 0.5,
anchorY: 0.5
}));
overlay.x = WIDTH / 2;
overlay.y = HEIGHT / 2;
overlay.alpha = 0.75;
var panel = scene.addChild(LK.getAsset('cratePanel', {
anchorX: 0.5,
anchorY: 0.5
}));
panel.x = WIDTH / 2;
panel.y = HEIGHT / 2;
makeLabel(scene, 'OPENING CRATE', WIDTH / 2, 420, 58, '#FFFFFF');
makeLabel(scene, 'The reward is rolled first, then the carousel lands on it.', WIDTH / 2, 485, 24, '#93c5fd');
var spinWindow = scene.addChild(LK.getAsset('spinWindow', {
anchorX: 0.5,
anchorY: 0.5
}));
spinWindow.x = WIDTH / 2;
spinWindow.y = 1100;
var centerMarker = scene.addChild(LK.getAsset('centerMarker', {
anchorX: 0.5,
anchorY: 0.5
}));
centerMarker.x = WIDTH / 2;
centerMarker.y = 1100;
crateWinningReward = sanitizeCrateReward(weightedRewardRoll(crateRewardPool));
cratePendingReward = null;
crateSpinFinished = false;
var visualData = buildSpinVisualRewards(crateWinningReward);
crateSpinData = visualData.items;
crateSpinCards = [];
crateSpinRoot = scene.addChild(new Container());
crateSpinRoot.x = 0;
crateSpinRoot.y = 0;
crateSpinStrip = crateSpinRoot.addChild(new Container());
var spacing = 240;
var startX = WIDTH / 2 - 200;
for (var i = 0; i < crateSpinData.length; i++) {
var cardX = startX + i * spacing;
makeRewardCard(crateSpinStrip, crateSpinData[i], cardX, 1100);
crateSpinCards.push({
x: cardX,
reward: crateSpinData[i]
});
}
var winningCardX = startX + visualData.winIndex * spacing;
crateSpinStrip.x = 0;
crateSpinTargetX = WIDTH / 2 - winningCardX;
crateSpinVelocity = -85;
crateSpinning = true;
crateResultTitle = makeLabel(scene, 'Spinning...', WIDTH / 2, 1450, 42, '#facc15');
crateResultSubtitle = makeLabel(scene, 'Waiting for reward', WIDTH / 2, 1510, 26, '#FFFFFF');
crateClaimButton = scene.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5
}));
crateClaimButton.x = WIDTH / 2;
crateClaimButton.y = 1880;
crateClaimButton.alpha = 0.35;
makeLabel(scene, 'CLAIM', WIDTH / 2, 1880, 36, '#08110d');
crateBackButton = scene.addChild(LK.getAsset('buttonAlt', {
anchorX: 0.5,
anchorY: 0.5
}));
crateBackButton.x = WIDTH / 2;
crateBackButton.y = 2005;
crateBackButton.alpha = 0.35;
makeLabel(scene, 'BACK TO GARAGE', WIDTH / 2, 2005, 30, '#eff6ff');
crateClaimButton.down = function () {
if (!crateSpinFinished || !crateWinningReward) return;
if (!cratePendingReward) {
cratePendingReward = applyCrateReward(crateWinningReward);
crateResultTitle.setText(cratePendingReward.title);
crateResultSubtitle.setText(cratePendingReward.subtitle);
}
initGarage();
};
crateBackButton.down = function () {
if (!crateSpinFinished) return;
initGarage();
};
game.down = function () {};
game.update = function () {
if (currentState !== 'crate_opening') return;
if (crateSpinning) {
var delta = crateSpinTargetX - crateSpinStrip.x;
crateSpinVelocity *= 0.965;
if (Math.abs(delta) > 10) {
crateSpinStrip.x += delta * 0.06 + crateSpinVelocity;
} else {
crateSpinStrip.x += delta * 0.18;
}
if (Math.abs(delta) < 2.5 && Math.abs(crateSpinVelocity) < 0.35) {
crateSpinStrip.x = crateSpinTargetX;
crateSpinning = false;
crateSpinFinished = true;
crateResultTitle.setText(crateWinningReward.label);
crateResultSubtitle.setText('Tap CLAIM to apply reward');
crateClaimButton.alpha = 1;
crateBackButton.alpha = 1;
}
}
};
}
/****
* Start
****/
initGarage(); /****
* Plugins
****/
var storage = LK.import("@upit/storage.v1", {
playerCash: 5000,
playerTier: 0,
tierXP: 0,
winStreak: 0,
bestTime: 0,
raceCount: 0,
crateCount: 0,
carColor: 14427686,
ownedColors: [14427686],
engineLevel: 0,
turboLevel: 0,
transmissionLevel: 0,
suspensionLevel: 0,
weightLevel: 0,
tireLevel: 0,
challengeRaceCount: 0,
challengeWins: 0,
challengePerfectLaunches: 0,
currentEventId: "street_sprint",
ladderStage: 0
});
/****
* Initialize Game
****/
/****
* Game
****/
var game = new LK.Game({
backgroundColor: 0x0b1020
});
/****
* Game Code
****/
/****
* Globals
****/
var crateRewardPool = [{
id: 'cash_small',
type: 'cash',
amount: 300,
weight: 28,
rarity: 'common',
label: '$300 Cash'
}, {
id: 'cash_mid',
type: 'cash',
amount: 700,
weight: 14,
rarity: 'uncommon',
label: '$700 Cash'
}, {
id: 'cash_big',
type: 'cash',
amount: 1400,
weight: 5,
rarity: 'rare',
label: '$1400 Cash'
}, {
id: 'engine_up',
type: 'upgrade',
key: 'engine',
levels: 1,
weight: 10,
rarity: 'rare',
label: 'Engine +1'
}, {
id: 'turbo_up',
type: 'upgrade',
key: 'turbo',
levels: 1,
weight: 9,
rarity: 'rare',
label: 'Turbo +1'
}, {
id: 'trans_up',
type: 'upgrade',
key: 'transmission',
levels: 1,
weight: 10,
rarity: 'uncommon',
label: 'Transmission +1'
}, {
id: 'susp_up',
type: 'upgrade',
key: 'suspension',
levels: 1,
weight: 10,
rarity: 'uncommon',
label: 'Suspension +1'
}, {
id: 'weight_up',
type: 'upgrade',
key: 'weight',
levels: 1,
weight: 8,
rarity: 'rare',
label: 'Weight Reduction +1'
}, {
id: 'tire_up',
type: 'upgrade',
key: 'tire',
levels: 1,
weight: 11,
rarity: 'uncommon',
label: 'Tires +1'
}, {
id: 'paint_unlock',
type: 'cosmetic',
reward: 'paint',
weight: 10,
rarity: 'epic',
label: 'Random Paint'
}, {
id: 'bonus_crate',
type: 'crate',
amount: 1,
weight: 2,
rarity: 'legendary',
label: '+1 Bonus Crate'
}];
var crateSpinRoot = null;
var crateSpinStrip = null;
var crateSpinCards = [];
var crateSpinData = [];
var crateSpinVelocity = 0;
var crateSpinTargetX = 0;
var crateSpinning = false;
var crateSpinFinished = false;
var crateWinningReward = null;
var cratePendingReward = null;
var crateResultTitle = null;
var crateResultSubtitle = null;
var crateClaimButton = null;
var crateBackButton = null;
var WIDTH = 2048;
var HEIGHT = 2732;
var scene = null;
var currentState = 'garage';
var playerCar = null;
var opponentCar = null;
var launchLights = [];
var rpmText = null;
var speedText = null;
var gearText = null;
var distanceText = null;
var promptText = null;
var eventInfoText = null;
var raceTimer = 0;
var playerFinishTime = 0;
var opponentFinishTime = 0;
var countdownIndex = 0;
var countdownElapsed = 0;
var raceStarted = false;
var raceFinished = false;
var playerCanShift = false;
var playerLaunched = false;
var playerSpeed = 0;
var opponentSpeed = 0;
var playerDistance = 0;
var opponentDistance = 0;
var playerGear = 1;
var opponentGear = 1;
var playerRPM = 1000;
var launchAccuracy = 0;
var launchQuality = 'MISS';
var playerShiftWindowMin = 5200;
var playerShiftWindowMax = 6600;
var shiftScore = 0;
var missedShifts = 0;
var currentRaceDistance = 3000;
var currentOpponentBonus = 0;
var lastRaceSummary = null;
var foulStart = false;
var playerGearMax = 6;
var opponentGearMax = 6;
var shiftQualityCounts = {
perfect: 0,
good: 0,
late: 0,
early: 0,
bad: 0
};
var gearRatios = [0, 2.8, 2.1, 1.6, 1.3, 1.05, 0.88];
var opponentGearRatios = [0, 2.7, 2.05, 1.58, 1.28, 1.02, 0.86];
var upgradeData = [{
key: 'engine',
label: 'Engine',
baseCost: 1000,
maxLevel: 5
}, {
key: 'turbo',
label: 'Turbo',
baseCost: 1400,
maxLevel: 5
}, {
key: 'transmission',
label: 'Transmission',
baseCost: 900,
maxLevel: 5
}, {
key: 'suspension',
label: 'Suspension',
baseCost: 700,
maxLevel: 5
}, {
key: 'weight',
label: 'Weight Reduction',
baseCost: 1100,
maxLevel: 5
}, {
key: 'tire',
label: 'Tires',
baseCost: 600,
maxLevel: 5
}];
var eventDefinitions = [{
id: 'street_sprint',
name: 'Street Sprint',
type: 'normal',
baseReward: 900,
xpReward: 40,
distance: 3000,
tierRequired: 0,
launchMultiplier: 1.0,
shiftMultiplier: 1.0,
mistakeLimit: 99,
description: 'Balanced standard drag event.'
}, {
id: 'perfect_shift',
name: 'Perfect Shift Challenge',
type: 'shift',
baseReward: 1100,
xpReward: 50,
distance: 3200,
tierRequired: 1,
launchMultiplier: 0.9,
shiftMultiplier: 1.8,
mistakeLimit: 2,
description: 'Big rewards for clean shifts.'
}, {
id: 'reaction_test',
name: 'Reaction Test',
type: 'launch',
baseReward: 1200,
xpReward: 55,
distance: 2800,
tierRequired: 2,
launchMultiplier: 2.0,
shiftMultiplier: 0.8,
mistakeLimit: 99,
description: 'Launch quality matters most.'
}, {
id: 'rival_ladder',
name: 'Rival Ladder',
type: 'ladder',
baseReward: 1800,
xpReward: 80,
distance: 3000,
tierRequired: 3,
launchMultiplier: 1.2,
shiftMultiplier: 1.2,
mistakeLimit: 3,
description: 'Beat a sequence of rising rivals.'
}];
var colorPool = [0xdc2626, 0x2563eb, 0x22c55e, 0xf59e0b, 0xa855f7, 0xec4899, 0x14b8a6, 0xf97316];
/****
* Helpers
****/
function clearScene() {
if (scene) {
scene.destroy();
scene = null;
}
game.update = function () {};
game.down = function () {};
}
function formatCash(v) {
return '$' + Math.floor(v);
}
function getUpgradeLevel(key) {
return storage[key + 'Level'] || 0;
}
function getUpgradeCost(key, baseCost) {
var level = getUpgradeLevel(key);
return baseCost + level * Math.floor(baseCost * 0.65);
}
function getPlayerStats() {
var engine = getUpgradeLevel('engine');
var turbo = getUpgradeLevel('turbo');
var transmission = getUpgradeLevel('transmission');
var suspension = getUpgradeLevel('suspension');
var weight = getUpgradeLevel('weight');
var tire = getUpgradeLevel('tire');
return {
hp: 160 + engine * 28 + turbo * 36,
torque: 210 + engine * 22 + turbo * 30,
grip: 72 + tire * 6 + suspension * 4,
mass: Math.max(980, 1450 - weight * 55),
shiftSpeed: 1 + transmission * 0.08,
traction: 1 + tire * 0.06 + suspension * 0.03
};
}
function getOpponentStats() {
var tier = storage.playerTier || 0;
return {
hp: 170 + tier * 35 + currentOpponentBonus,
torque: 220 + tier * 28 + currentOpponentBonus * 0.75,
grip: 75 + tier * 5,
mass: 1425 - tier * 35,
shiftSpeed: 1.03 + tier * 0.05,
traction: 1.02 + tier * 0.05
};
}
function makeLabel(parent, txt, x, y, size, color, ax, ay) {
var t = parent.addChild(new Text2(txt, {
size: size || 32,
fill: color || '#FFFFFF'
}));
t.anchor.set(ax == null ? 0.5 : ax, ay == null ? 0.5 : ay);
t.x = x;
t.y = y;
return t;
}
function createCar(isOpponent) {
var c = new Container();
var body = c.addChild(LK.getAsset(isOpponent ? 'opponentBody' : 'carBody', {
anchorX: 0.5,
anchorY: 0.5
}));
var windowObj = c.addChild(LK.getAsset('carWindow', {
anchorX: 0.5,
anchorY: 0.5
}));
windowObj.y = -10;
var wheels = [];
var rims = [];
for (var i = 0; i < 4; i++) {
wheels.push(c.addChild(LK.getAsset('wheel', {
anchorX: 0.5,
anchorY: 0.5
})));
rims.push(c.addChild(LK.getAsset('rim', {
anchorX: 0.5,
anchorY: 0.5
})));
}
var positions = [{
x: -45,
y: -36
}, {
x: 45,
y: -36
}, {
x: -45,
y: 36
}, {
x: 45,
y: 36
}];
for (var j = 0; j < 4; j++) {
wheels[j].x = positions[j].x;
wheels[j].y = positions[j].y;
rims[j].x = positions[j].x;
rims[j].y = positions[j].y;
}
c.setColor = function (color) {
body.tint = color;
};
c.spinWheels = function (speed) {
for (var k = 0; k < rims.length; k++) {
rims[k].rotation += speed;
}
};
return c;
}
function createProgressBar(parent, x, y, width, ratio, fillColor) {
var bar = new Container();
parent.addChild(bar);
bar.x = x;
bar.y = y;
var bg = bar.addChild(LK.getAsset('progressBg', {
anchorX: 0,
anchorY: 0.5
}));
bg.width = width;
var fill = bar.addChild(LK.getAsset('progressFill', {
anchorX: 0,
anchorY: 0.5
}));
fill.width = Math.max(0, Math.min(width, width * ratio));
fill.tint = fillColor || 0x22c55e;
return {
root: bar,
bg: bg,
fill: fill
};
}
function resetRaceValues() {
raceTimer = 0;
playerFinishTime = 0;
opponentFinishTime = 0;
countdownIndex = 0;
countdownElapsed = 0;
raceStarted = false;
raceFinished = false;
playerCanShift = false;
playerLaunched = false;
playerSpeed = 0;
opponentSpeed = 0;
playerDistance = 0;
opponentDistance = 0;
playerGear = 1;
opponentGear = 1;
playerRPM = 1000;
launchAccuracy = 0;
launchQuality = 'MISS';
shiftScore = 0;
missedShifts = 0;
currentOpponentBonus = 0;
foulStart = false;
shiftQualityCounts = {
perfect: 0,
good: 0,
late: 0,
early: 0,
bad: 0
};
if (opponentCar) opponentCar.aiRPM = 3200;
}
function getXPNeededForNextTier() {
return 100 + (storage.playerTier || 0) * 40;
}
function addTierXP(amount) {
storage.tierXP = (storage.tierXP || 0) + amount;
var leveledUp = false;
while (storage.tierXP >= getXPNeededForNextTier()) {
storage.tierXP -= getXPNeededForNextTier();
storage.playerTier = (storage.playerTier || 0) + 1;
storage.crateCount = (storage.crateCount || 0) + 1;
leveledUp = true;
}
return leveledUp;
}
function getChallengeSet() {
return [{
id: 'race3',
label: 'Run 3 races',
progress: storage.challengeRaceCount || 0,
target: 3,
reward: 350
}, {
id: 'win2',
label: 'Win 2 races',
progress: storage.challengeWins || 0,
target: 2,
reward: 500
}, {
id: 'perfect1',
label: 'Hit 1 perfect launch',
progress: storage.challengePerfectLaunches || 0,
target: 1,
reward: 450
}];
}
function applyChallengeRewards() {
var rewards = [];
var set = getChallengeSet();
for (var i = 0; i < set.length; i++) {
var c = set[i];
if (c.progress >= c.target) {
storage.playerCash += c.reward;
rewards.push(c.label + ' +' + formatCash(c.reward));
}
}
if ((storage.challengeRaceCount || 0) >= 3) storage.challengeRaceCount = 0;
if ((storage.challengeWins || 0) >= 2) storage.challengeWins = 0;
if ((storage.challengePerfectLaunches || 0) >= 1) storage.challengePerfectLaunches = 0;
return rewards;
}
function unlockRandomCosmetic() {
var owned = storage.ownedColors || [0xdc2626];
var available = [];
for (var i = 0; i < colorPool.length; i++) {
var found = false;
for (var j = 0; j < owned.length; j++) {
if (owned[j] === colorPool[i]) {
found = true;
break;
}
}
if (!found) available.push(colorPool[i]);
}
if (available.length <= 0) {
storage.playerCash += 300;
return {
type: 'cash',
value: 300
};
}
var color = available[Math.floor(Math.random() * available.length)];
owned.push(color);
storage.ownedColors = owned;
storage.carColor = color;
return {
type: 'color',
value: color
};
}
function getCurrentEvent() {
for (var i = 0; i < eventDefinitions.length; i++) {
if (eventDefinitions[i].id === storage.currentEventId) return eventDefinitions[i];
}
return eventDefinitions[0];
}
function isEventUnlocked(eventDef) {
return (storage.playerTier || 0) >= eventDef.tierRequired;
}
function getNextLockedEvent() {
for (var i = 0; i < eventDefinitions.length; i++) {
if (!isEventUnlocked(eventDefinitions[i])) return eventDefinitions[i];
}
return null;
}
function getRewardCardAssetByRarity(rarity) {
if (rarity === 'legendary') return 'rewardCardLegendary';
if (rarity === 'epic') return 'rewardCardEpic';
if (rarity === 'rare') return 'rewardCardRare';
if (rarity === 'uncommon') return 'rewardCardUncommon';
return 'rewardCardCommon';
}
function cloneRewardEntry(entry) {
return {
id: entry.id,
type: entry.type,
amount: entry.amount,
key: entry.key,
levels: entry.levels,
weight: entry.weight,
rarity: entry.rarity,
reward: entry.reward,
label: entry.label
};
}
function weightedRewardRoll(pool) {
var totalWeight = 0;
for (var i = 0; i < pool.length; i++) totalWeight += pool[i].weight;
var roll = Math.random() * totalWeight;
var running = 0;
for (var j = 0; j < pool.length; j++) {
running += pool[j].weight;
if (roll <= running) return cloneRewardEntry(pool[j]);
}
return cloneRewardEntry(pool[pool.length - 1]);
}
function getUpgradeMaxLevel(key) {
for (var i = 0; i < upgradeData.length; i++) {
if (upgradeData[i].key === key) return upgradeData[i].maxLevel;
}
return 5;
}
function getRandomUnownedColor() {
var owned = storage.ownedColors || [0xdc2626];
var available = [];
for (var i = 0; i < colorPool.length; i++) {
var found = false;
for (var j = 0; j < owned.length; j++) {
if (owned[j] === colorPool[i]) {
found = true;
break;
}
}
if (!found) available.push(colorPool[i]);
}
if (available.length <= 0) return null;
return available[Math.floor(Math.random() * available.length)];
}
function sanitizeCrateReward(reward) {
var result = cloneRewardEntry(reward);
if (result.type === 'upgrade') {
var current = getUpgradeLevel(result.key);
var maxLevel = getUpgradeMaxLevel(result.key);
if (current >= maxLevel) {
return {
id: 'fallback_cash_' + result.key,
type: 'cash',
amount: 900,
rarity: 'uncommon',
label: 'Upgrade Maxed → $900'
};
}
}
if (result.type === 'cosmetic' && result.reward === 'paint') {
var color = getRandomUnownedColor();
if (color == null) {
return {
id: 'fallback_cash_paint',
type: 'cash',
amount: 600,
rarity: 'uncommon',
label: 'Duplicate Paint → $600'
};
}
result.colorValue = color;
result.label = 'New Paint';
}
return result;
}
function applyCrateReward(reward) {
if (!reward) {
return {
title: 'No Reward',
subtitle: 'Nothing was applied.'
};
}
if (reward.type === 'cash') {
storage.playerCash = (storage.playerCash || 0) + (reward.amount || 0);
return {
title: reward.label,
subtitle: 'Cash added to your garage funds.'
};
}
if (reward.type === 'crate') {
storage.crateCount = (storage.crateCount || 0) + (reward.amount || 1);
return {
title: reward.label,
subtitle: 'A bonus crate was added.'
};
}
if (reward.type === 'upgrade') {
var current = getUpgradeLevel(reward.key);
var maxLevel = getUpgradeMaxLevel(reward.key);
var levelsToAdd = reward.levels || 1;
var nextLevel = Math.min(maxLevel, current + levelsToAdd);
storage[reward.key + 'Level'] = nextLevel;
return {
title: reward.label,
subtitle: reward.key + ' upgraded to level ' + nextLevel + '.'
};
}
if (reward.type === 'cosmetic' && reward.reward === 'paint') {
var owned = storage.ownedColors || [0xdc2626];
owned.push(reward.colorValue);
storage.ownedColors = owned;
storage.carColor = reward.colorValue;
return {
title: reward.label,
subtitle: 'New paint unlocked and equipped.'
};
}
return {
title: reward.label || 'Reward',
subtitle: 'Reward claimed.'
};
}
function getRandomPoolRewardForVisual() {
return cloneRewardEntry(crateRewardPool[Math.floor(Math.random() * crateRewardPool.length)]);
}
function buildSpinVisualRewards(winningReward) {
var items = [];
for (var i = 0; i < 36; i++) items.push(getRandomPoolRewardForVisual());
var winIndex = 29;
items[winIndex] = cloneRewardEntry(winningReward);
return {
items: items,
winIndex: winIndex
};
}
function makeRewardCard(parent, reward, x, y) {
var assetName = getRewardCardAssetByRarity(reward.rarity);
var root = parent.addChild(LK.getAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
}));
root.x = x;
root.y = y;
makeLabel(parent, reward.label || 'Reward', x, y - 22, 24, '#FFFFFF');
makeLabel(parent, (reward.rarity || 'common').toUpperCase(), x, y + 24, 18, '#e5e7eb');
return root;
}
/****
* Garage
****/
function initGarage() {
clearScene();
currentState = 'garage';
LK.playMusic('garageMusic', {
loop: true
});
scene = game.addChild(new Container());
makeLabel(scene, 'MIDNIGHT STRIP', WIDTH / 2, 90, 80, '#FFFFFF');
makeLabel(scene, 'Career Events • Build • Launch • Upgrade', WIDTH / 2, 160, 30, '#93c5fd');
var cashPanel = scene.addChild(LK.getAsset('widePanel', {
anchorX: 0.5,
anchorY: 0.5
}));
cashPanel.x = WIDTH / 2;
cashPanel.y = 300;
makeLabel(scene, 'Cash: ' + formatCash(storage.playerCash || 0), WIDTH / 2 - 300, 280, 36, '#22c55e', 0, 0.5);
makeLabel(scene, 'Tier ' + ((storage.playerTier || 0) + 1), WIDTH / 2 - 300, 330, 28, '#facc15', 0, 0.5);
makeLabel(scene, 'Win Streak: ' + (storage.winStreak || 0), WIDTH / 2 + 60, 280, 28, '#FFFFFF', 0, 0.5);
makeLabel(scene, 'Crates: ' + (storage.crateCount || 0), WIDTH / 2 + 60, 330, 28, '#FFFFFF', 0, 0.5);
var xpNeeded = getXPNeededForNextTier();
createProgressBar(scene, WIDTH / 2 - 300, 380, 560, (storage.tierXP || 0) / xpNeeded, 0x3b82f6);
makeLabel(scene, 'Tier Progress: ' + (storage.tierXP || 0) + ' / ' + xpNeeded, WIDTH / 2, 425, 22, '#cbd5e1');
playerCar = scene.addChild(createCar(false));
playerCar.x = 270;
playerCar.y = 700;
playerCar.scale.set(1.65);
playerCar.setColor(storage.carColor || 0xdc2626);
var stats = getPlayerStats();
makeLabel(scene, 'HP: ' + stats.hp, 110, 900, 30, '#FFFFFF', 0, 0.5);
makeLabel(scene, 'Torque: ' + stats.torque, 110, 945, 30, '#FFFFFF', 0, 0.5);
makeLabel(scene, 'Grip: ' + stats.grip, 110, 990, 30, '#FFFFFF', 0, 0.5);
makeLabel(scene, 'Weight: ' + stats.mass + 'kg', 110, 1035, 30, '#FFFFFF', 0, 0.5);
makeLabel(scene, 'Best Time: ' + (storage.bestTime ? storage.bestTime.toFixed(2) + 's' : '--'), 110, 1080, 30, '#93c5fd', 0, 0.5);
makeLabel(scene, 'EVENTS', 1280, 520, 40, '#facc15');
for (var e = 0; e < eventDefinitions.length; e++) {
(function (eventDef, index) {
var unlocked = isEventUnlocked(eventDef);
var isSelected = storage.currentEventId === eventDef.id;
var card = scene.addChild(LK.getAsset(unlocked ? 'eventCard' : 'eventLockedCard', {
anchorX: 0.5,
anchorY: 0.5
}));
card.x = 930 + index % 2 * 470;
card.y = 650 + Math.floor(index / 2) * 150;
if (isSelected && unlocked) card.tint = 0x2563eb;
makeLabel(scene, eventDef.name, card.x, card.y - 24, 28, '#FFFFFF');
makeLabel(scene, unlocked ? 'Reward ' + formatCash(eventDef.baseReward) : 'Unlocks at Tier ' + (eventDef.tierRequired + 1), card.x, card.y + 6, 20, unlocked ? '#22c55e' : '#facc15');
makeLabel(scene, eventDef.description, card.x, card.y + 34, 18, '#cbd5e1');
card.down = function () {
if (!unlocked) return;
storage.currentEventId = eventDef.id;
initGarage();
};
})(eventDefinitions[e], e);
}
var nextLocked = getNextLockedEvent();
makeLabel(scene, nextLocked ? 'Next Event: ' + nextLocked.name + ' at Tier ' + (nextLocked.tierRequired + 1) : 'All events unlocked', 1280, 980, 24, '#93c5fd');
makeLabel(scene, 'CHALLENGES', 1280, 1070, 40, '#facc15');
var challenges = getChallengeSet();
for (var c = 0; c < challenges.length; c++) {
var challenge = challenges[c];
var card2 = scene.addChild(LK.getAsset('challengeCard', {
anchorX: 0.5,
anchorY: 0.5
}));
card2.x = 1280;
card2.y = 1180 + c * 145;
makeLabel(scene, challenge.label, card2.x - 230, card2.y - 18, 28, '#FFFFFF', 0, 0.5);
makeLabel(scene, challenge.progress + ' / ' + challenge.target, card2.x - 230, card2.y + 18, 22, '#93c5fd', 0, 0.5);
makeLabel(scene, formatCash(challenge.reward), card2.x + 180, card2.y, 22, '#22c55e');
}
makeLabel(scene, 'UPGRADES', WIDTH / 2, 1620, 40, '#facc15');
for (var i = 0; i < upgradeData.length; i++) {
(function (data, index) {
var level = getUpgradeLevel(data.key);
var cost = getUpgradeCost(data.key, data.baseCost);
var card3 = scene.addChild(LK.getAsset('upgradeCard', {
anchorX: 0.5,
anchorY: 0.5
}));
card3.x = 560 + index % 2 * 930;
card3.y = 1740 + Math.floor(index / 2) * 120;
makeLabel(scene, data.label + ' Lv.' + level, card3.x, card3.y - 18, 28, '#FFFFFF');
makeLabel(scene, level >= data.maxLevel ? 'MAXED' : 'Buy ' + formatCash(cost), card3.x, card3.y + 18, 22, level >= data.maxLevel ? '#facc15' : '#22c55e');
card3.down = function () {
buyUpgrade(data);
};
})(upgradeData[i], i);
}
var selectedEvent = getCurrentEvent();
makeLabel(scene, 'Selected Event: ' + selectedEvent.name, WIDTH / 2, 2230, 32, '#FFFFFF');
makeLabel(scene, 'Type: ' + selectedEvent.type + ' • Distance: ' + selectedEvent.distance + 'm • XP: ' + selectedEvent.xpReward, WIDTH / 2, 2280, 24, '#93c5fd');
var raceBtn = scene.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5
}));
raceBtn.x = WIDTH / 2;
raceBtn.y = 2400;
makeLabel(scene, 'START EVENT', WIDTH / 2, 2400, 38, '#08110d');
raceBtn.down = function () {
startCountdown();
};
var crateBtn = scene.addChild(LK.getAsset('buttonAlt', {
anchorX: 0.5,
anchorY: 0.5
}));
crateBtn.x = WIDTH / 2;
crateBtn.y = 2520;
makeLabel(scene, 'OPEN CRATE', WIDTH / 2, 2520, 34, '#eff6ff');
crateBtn.down = function () {
openCratePopup();
};
}
function buyUpgrade(data) {
var level = getUpgradeLevel(data.key);
if (level >= data.maxLevel) return;
var cost = getUpgradeCost(data.key, data.baseCost);
if ((storage.playerCash || 0) < cost) return;
storage.playerCash -= cost;
storage[data.key + 'Level'] = level + 1;
LK.getSound('cashReward').play();
initGarage();
}
/****
* Countdown
****/
function startCountdown() {
resetRaceValues();
clearScene();
currentState = 'countdown';
LK.playMusic('raceMusic', {
loop: true
});
var eventDef = getCurrentEvent();
currentRaceDistance = eventDef.distance;
if (eventDef.type === 'ladder') currentOpponentBonus = (storage.ladderStage || 0) * 20;
scene = game.addChild(new Container());
var track = scene.addChild(LK.getAsset('track', {
anchorX: 0.5,
anchorY: 0.5
}));
track.x = WIDTH / 2;
track.y = 1500;
var finish = scene.addChild(LK.getAsset('finishLine', {
anchorX: 0.5,
anchorY: 0.5
}));
finish.x = 1760;
finish.y = 1500;
playerCar = scene.addChild(createCar(false));
playerCar.x = 340;
playerCar.y = 1380;
playerCar.setColor(storage.carColor || 0xdc2626);
opponentCar = scene.addChild(createCar(true));
opponentCar.x = 340;
opponentCar.y = 1620;
opponentCar.aiRPM = 3200;
makeLabel(scene, eventDef.name, WIDTH / 2, 150, 54, '#FFFFFF');
makeLabel(scene, eventDef.description, WIDTH / 2, 215, 26, '#facc15');
launchLights = [];
for (var i = 0; i < 3; i++) {
var light = scene.addChild(LK.getAsset('lightOff', {
anchorX: 0.5,
anchorY: 0.5
}));
light.x = WIDTH / 2;
light.y = 520 + i * 72;
launchLights.push(light);
}
rpmText = makeLabel(scene, 'RPM: 1000', WIDTH / 2, 920, 42, '#22c55e');
promptText = makeLabel(scene, 'Tap on GREEN', WIDTH / 2, 1000, 34, '#FFFFFF');
eventInfoText = makeLabel(scene, 'Distance: ' + currentRaceDistance + 'm', WIDTH / 2, 1060, 26, '#93c5fd');
game.down = function () {
if (currentState !== 'countdown') return;
if (countdownIndex < 3) {
foulStart = true;
launchQuality = 'FOUL';
launchAccuracy = 0.15;
playerLaunched = true;
LK.getSound('launch').play();
beginRace();
return;
}
foulStart = false;
playerLaunched = true;
var rpmScore = 1 - Math.min(1, Math.abs(5000 - playerRPM) / 2200);
launchAccuracy = Math.max(0, rpmScore);
if (launchAccuracy > 0.88) launchQuality = 'PERFECT';else if (launchAccuracy > 0.68) launchQuality = 'GOOD';else if (launchAccuracy > 0.45) launchQuality = 'OK';else launchQuality = 'POOR';
LK.getSound('launch').play();
beginRace();
};
game.update = function () {
if (currentState !== 'countdown') return;
countdownElapsed += 1 / 60;
playerRPM += 85;
if (playerRPM > 7000) playerRPM = 3200;
rpmText.setText('RPM: ' + Math.floor(playerRPM));
if (countdownElapsed >= 0.75 && countdownIndex < 3) {
countdownElapsed = 0;
launchLights[countdownIndex].destroy();
launchLights[countdownIndex] = scene.addChild(LK.getAsset(countdownIndex < 2 ? 'lightRed' : 'lightGreen', {
anchorX: 0.5,
anchorY: 0.5
}));
launchLights[countdownIndex].x = WIDTH / 2;
launchLights[countdownIndex].y = 520 + countdownIndex * 72;
countdownIndex++;
}
if (countdownIndex >= 3 && !playerLaunched && countdownElapsed >= 0.40) {
foulStart = false;
launchQuality = 'LATE';
launchAccuracy = 0.2;
playerLaunched = true;
beginRace();
}
};
}
/****
* Race
****/
function beginRace() {
currentState = 'race';
raceStarted = true;
raceFinished = false;
raceTimer = 0;
playerCanShift = false;
var playerStats = getPlayerStats();
var aiStats = getOpponentStats();
var eventDef = getCurrentEvent();
speedText = makeLabel(scene, 'Speed: 0 mph', 150, 120, 34, '#22c55e', 0, 0.5);
gearText = makeLabel(scene, 'Gear: 1', 150, 170, 34, '#facc15', 0, 0.5);
distanceText = makeLabel(scene, 'Distance: 0 m', 150, 220, 34, '#FFFFFF', 0, 0.5);
promptText.setText('Tap to shift through the gears');
game.down = function () {
if (currentState !== 'race' || !raceStarted || raceFinished) return;
var shiftCenter = (playerShiftWindowMin + playerShiftWindowMax) * 0.5;
var shiftDelta = playerRPM - shiftCenter;
if (playerGear >= playerGearMax) {
promptText.setText('TOP GEAR');
return;
}
if (playerRPM < playerShiftWindowMin - 350) {
shiftQualityCounts.early++;
missedShifts++;
playerGear++;
playerRPM = 3400;
playerSpeed *= 0.96;
promptText.setText('EARLY SHIFT');
LK.getSound('shiftGear').play();
} else if (playerRPM >= playerShiftWindowMin && playerRPM <= playerShiftWindowMax) {
if (Math.abs(shiftDelta) < 160) {
shiftQualityCounts.perfect++;
shiftScore += 3;
promptText.setText('PERFECT SHIFT');
} else {
shiftQualityCounts.good++;
shiftScore += 2;
promptText.setText('GOOD SHIFT');
}
playerGear++;
playerRPM = 3900 - getUpgradeLevel('transmission') * 140;
playerSpeed *= 1.015;
LK.getSound('shiftGear').play();
} else if (playerRPM > playerShiftWindowMax && playerRPM <= playerShiftWindowMax + 550) {
shiftQualityCounts.late++;
shiftScore += 1;
missedShifts++;
playerGear++;
playerRPM = 4300;
playerSpeed *= 0.985;
promptText.setText('LATE SHIFT');
LK.getSound('shiftGear').play();
} else {
shiftQualityCounts.bad++;
missedShifts++;
playerSpeed *= 0.95;
promptText.setText('BAD SHIFT');
}
playerCanShift = false;
};
game.update = function () {
if (currentState !== 'race' || raceFinished) return;
raceTimer += 1 / 60;
var currentRatio = gearRatios[playerGear] || 0.88;
var opponentRatio = opponentGearRatios[opponentGear] || 0.86;
var playerLaunchBoost = foulStart ? 0.68 : 0.78 + launchAccuracy * 0.7;
var playerAccel = playerStats.torque * currentRatio / playerStats.mass * 1.18 * playerLaunchBoost;
var aiLaunchBoost = 0.9 + (storage.playerTier || 0) * 0.03;
var aiAccel = aiStats.torque * opponentRatio / aiStats.mass * 1.1 * aiLaunchBoost;
playerRPM += 85 + playerGear * 32 + getUpgradeLevel('engine') * 8 + playerSpeed * 0.045;
if (playerRPM >= playerShiftWindowMin && playerRPM <= playerShiftWindowMax) {
playerCanShift = true;
promptText.setText('SHIFT NOW');
}
if (playerRPM > playerShiftWindowMax + 650) {
playerRPM = 6850;
playerSpeed *= 0.992;
promptText.setText('ON LIMITER');
}
if (playerGear <= playerGearMax) {
playerSpeed += playerAccel;
}
if (!opponentCar.aiRPM) opponentCar.aiRPM = 3200;
opponentCar.aiRPM += 95 + opponentGear * 24 + opponentSpeed * 0.035;
var aiShiftThreshold = 6100 + (storage.playerTier || 0) * 70 + currentOpponentBonus * 2;
if (opponentGear < opponentGearMax && opponentCar.aiRPM >= aiShiftThreshold) {
opponentGear++;
opponentCar.aiRPM = 4100;
}
opponentSpeed += aiAccel;
playerSpeed = Math.min(playerSpeed, 60 + playerStats.hp * 0.62);
opponentSpeed = Math.min(opponentSpeed, 60 + aiStats.hp * 0.6);
playerDistance += playerSpeed * 0.17;
opponentDistance += opponentSpeed * 0.17;
playerCar.x = 340 + Math.min(1320, playerDistance / currentRaceDistance * 1320);
opponentCar.x = 340 + Math.min(1320, opponentDistance / currentRaceDistance * 1320);
playerCar.spinWheels(playerSpeed * 0.01);
opponentCar.spinWheels(opponentSpeed * 0.01);
speedText.setText('Speed: ' + Math.floor(playerSpeed) + ' mph');
gearText.setText('Gear: ' + playerGear);
distanceText.setText('Distance: ' + Math.floor(playerDistance) + ' m');
if (!playerFinishTime && playerDistance >= currentRaceDistance) playerFinishTime = raceTimer;
if (!opponentFinishTime && opponentDistance >= currentRaceDistance) opponentFinishTime = raceTimer;
if (playerFinishTime && opponentFinishTime) {
raceFinished = true;
processRaceResults();
}
};
}
/****
* Results processing
****/
function processRaceResults() {
var eventDef = getCurrentEvent();
var playerWon = playerFinishTime <= opponentFinishTime;
var raceCash = playerWon ? eventDef.baseReward : Math.floor(eventDef.baseReward * 0.35);
var launchBase = launchQuality === 'PERFECT' ? 300 : launchQuality === 'GOOD' ? 180 : launchQuality === 'OK' ? 100 : launchQuality === 'POOR' ? 40 : 0;
var launchBonus = Math.floor(launchBase * eventDef.launchMultiplier);
var shiftBonus = Math.floor(shiftScore * 70 * eventDef.shiftMultiplier);
var shiftPenalty = 0;
var eventBonus = 0;
var foulPenalty = foulStart ? 250 : 0;
if (eventDef.type === 'shift') {
shiftPenalty = missedShifts * 60;
if (missedShifts <= eventDef.mistakeLimit) eventBonus += 250;
}
if (eventDef.type === 'launch' && launchQuality === 'PERFECT') {
eventBonus += 350;
}
var streakBonus = 0;
var personalBestBonus = 0;
var challengeRewards = [];
var tierUp = false;
var crateEarned = false;
var newBest = false;
var ladderAdvanced = false;
if (playerWon) {
storage.winStreak = (storage.winStreak || 0) + 1;
storage.challengeWins = (storage.challengeWins || 0) + 1;
} else {
storage.winStreak = 0;
}
if ((storage.winStreak || 0) > 0) streakBonus = Math.min(400, (storage.winStreak || 0) * 60);
if (launchQuality === 'PERFECT') {
storage.challengePerfectLaunches = (storage.challengePerfectLaunches || 0) + 1;
}
storage.challengeRaceCount = (storage.challengeRaceCount || 0) + 1;
storage.raceCount = (storage.raceCount || 0) + 1;
if (!storage.bestTime || playerFinishTime < storage.bestTime) {
storage.bestTime = playerFinishTime;
personalBestBonus = 250;
newBest = true;
}
var xpGain = eventDef.xpReward + (playerWon ? 10 : 0);
if (launchQuality === 'PERFECT') xpGain += 10;
if (newBest) xpGain += 10;
if (eventDef.type === 'ladder') {
if (playerWon) {
storage.ladderStage = (storage.ladderStage || 0) + 1;
ladderAdvanced = true;
if ((storage.ladderStage || 0) >= 3) {
eventBonus += 500;
storage.ladderStage = 0;
}
} else {
storage.ladderStage = 0;
}
}
var oldCrates = storage.crateCount || 0;
tierUp = addTierXP(xpGain);
crateEarned = (storage.crateCount || 0) > oldCrates;
challengeRewards = applyChallengeRewards();
var challengeCash = 0;
for (var i = 0; i < challengeRewards.length; i++) {
if (challengeRewards[i].indexOf('$') >= 0) {
var amount = parseInt(challengeRewards[i].split('$')[1], 10);
if (!isNaN(amount)) challengeCash += amount;
}
}
var totalEarned = raceCash + launchBonus + shiftBonus - shiftPenalty - foulPenalty + streakBonus + personalBestBonus + eventBonus + challengeCash;
storage.playerCash = (storage.playerCash || 0) + raceCash + launchBonus + shiftBonus - shiftPenalty - foulPenalty + streakBonus + personalBestBonus + eventBonus;
lastRaceSummary = {
eventName: eventDef.name,
playerWon: playerWon,
launchQuality: launchQuality,
playerTime: playerFinishTime,
opponentTime: opponentFinishTime,
raceCash: raceCash,
launchBonus: launchBonus,
shiftBonus: shiftBonus,
shiftPenalty: shiftPenalty,
foulStart: foulStart,
foulPenalty: foulPenalty,
eventBonus: eventBonus,
streakBonus: streakBonus,
personalBestBonus: personalBestBonus,
challengeRewards: challengeRewards,
totalEarned: totalEarned,
xpGain: xpGain,
tierUp: tierUp,
crateEarned: crateEarned,
ladderAdvanced: ladderAdvanced,
shiftBreakdown: {
perfect: shiftQualityCounts.perfect,
good: shiftQualityCounts.good,
late: shiftQualityCounts.late,
early: shiftQualityCounts.early,
bad: shiftQualityCounts.bad
},
nextUnlockText: getNextLockedEvent() ? 'Reach Tier ' + (getNextLockedEvent().tierRequired + 1) + ' to unlock ' + getNextLockedEvent().name : 'All event types unlocked'
};
if (playerWon) LK.getSound('raceWin').play();else LK.getSound('raceLose').play();
showResults();
}
/****
* Results screen
****/
function showResults() {
currentState = 'results';
clearScene();
scene = game.addChild(new Container());
var s = lastRaceSummary;
makeLabel(scene, s.playerWon ? 'EVENT WON' : 'EVENT LOST', WIDTH / 2, 120, 82, s.playerWon ? '#22c55e' : '#ef4444');
makeLabel(scene, s.eventName, WIDTH / 2, 190, 30, '#93c5fd');
makeLabel(scene, 'You Earned', WIDTH / 2, 320, 38, '#facc15');
makeLabel(scene, 'Race: ' + formatCash(s.raceCash), WIDTH / 2, 390, 30, '#FFFFFF');
makeLabel(scene, 'Launch Bonus: ' + formatCash(s.launchBonus) + ' (' + s.launchQuality + ')', WIDTH / 2, 440, 30, '#FFFFFF');
makeLabel(scene, 'Shift Bonus: ' + formatCash(s.shiftBonus), WIDTH / 2, 490, 30, '#FFFFFF');
makeLabel(scene, 'Shift Penalty: -' + formatCash(s.shiftPenalty), WIDTH / 2, 540, 30, '#FFFFFF');
makeLabel(scene, 'Foul Penalty: -' + formatCash(s.foulPenalty), WIDTH / 2, 590, 30, '#FFFFFF');
makeLabel(scene, 'Event Bonus: ' + formatCash(s.eventBonus), WIDTH / 2, 640, 30, '#FFFFFF');
makeLabel(scene, 'Streak Bonus: ' + formatCash(s.streakBonus), WIDTH / 2, 690, 30, '#FFFFFF');
makeLabel(scene, 'Personal Best: ' + formatCash(s.personalBestBonus), WIDTH / 2, 740, 30, '#FFFFFF');
makeLabel(scene, 'Total: ' + formatCash(s.totalEarned), WIDTH / 2, 810, 42, '#22c55e');
makeLabel(scene, 'Performance', WIDTH / 2, 940, 38, '#facc15');
makeLabel(scene, 'Your Time: ' + s.playerTime.toFixed(2) + 's', WIDTH / 2, 1010, 30, '#FFFFFF');
makeLabel(scene, 'Opponent Time: ' + s.opponentTime.toFixed(2) + 's', WIDTH / 2, 1060, 30, '#FFFFFF');
makeLabel(scene, 'XP Gained: ' + s.xpGain, WIDTH / 2, 1110, 30, '#93c5fd');
makeLabel(scene, 'Win Streak: ' + (storage.winStreak || 0), WIDTH / 2, 1160, 30, '#FFFFFF');
makeLabel(scene, 'Perfect Shifts: ' + s.shiftBreakdown.perfect, WIDTH / 2, 1210, 28, '#93c5fd');
makeLabel(scene, 'Good / Late / Early / Bad: ' + s.shiftBreakdown.good + ' / ' + s.shiftBreakdown.late + ' / ' + s.shiftBreakdown.early + ' / ' + s.shiftBreakdown.bad, WIDTH / 2, 1255, 24, '#FFFFFF');
if (s.ladderAdvanced) {
makeLabel(scene, 'Rival Ladder Progress: ' + (storage.ladderStage || 0) + ' / 3', WIDTH / 2, 1300, 28, '#facc15');
}
makeLabel(scene, 'Challenge Rewards', WIDTH / 2, 1450, 38, '#facc15');
if (s.challengeRewards.length <= 0) {
makeLabel(scene, 'No challenge completed this event', WIDTH / 2, 1520, 28, '#cbd5e1');
} else {
for (var i = 0; i < s.challengeRewards.length; i++) {
makeLabel(scene, s.challengeRewards[i], WIDTH / 2, 1520 + i * 42, 26, '#22c55e');
}
}
makeLabel(scene, 'Next Unlock', WIDTH / 2, 1790, 38, '#facc15');
makeLabel(scene, s.nextUnlockText, WIDTH / 2, 1860, 28, '#FFFFFF');
if (s.tierUp) makeLabel(scene, 'Tier Up! A new crate was added to your garage', WIDTH / 2, 1910, 26, '#22c55e');
if (s.crateEarned) makeLabel(scene, 'Cosmetic crate earned', WIDTH / 2, 1950, 26, '#93c5fd');
var carPreview = scene.addChild(createCar(false));
carPreview.x = WIDTH / 2;
carPreview.y = 2230;
carPreview.scale.set(1.65);
carPreview.setColor(storage.carColor || 0xdc2626);
var garageBtn = scene.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5
}));
garageBtn.x = WIDTH / 2;
garageBtn.y = 2520;
makeLabel(scene, 'BACK TO GARAGE', WIDTH / 2, 2520, 36, '#08110d');
garageBtn.down = function () {
initGarage();
};
}
/***********************
* CRATE UI
***********************/
function openCratePopup() {
if ((storage.crateCount || 0) <= 0) return;
storage.crateCount -= 1;
clearScene();
currentState = 'crate_opening';
scene = game.addChild(new Container());
var overlay = scene.addChild(LK.getAsset('overlayBg', {
anchorX: 0.5,
anchorY: 0.5
}));
overlay.x = WIDTH / 2;
overlay.y = HEIGHT / 2;
overlay.alpha = 0.75;
var panel = scene.addChild(LK.getAsset('cratePanel', {
anchorX: 0.5,
anchorY: 0.5
}));
panel.x = WIDTH / 2;
panel.y = HEIGHT / 2;
makeLabel(scene, 'OPENING CRATE', WIDTH / 2, 420, 58, '#FFFFFF');
makeLabel(scene, 'The reward is rolled first, then the carousel lands on it.', WIDTH / 2, 485, 24, '#93c5fd');
var spinWindow = scene.addChild(LK.getAsset('spinWindow', {
anchorX: 0.5,
anchorY: 0.5
}));
spinWindow.x = WIDTH / 2;
spinWindow.y = 1100;
var centerMarker = scene.addChild(LK.getAsset('centerMarker', {
anchorX: 0.5,
anchorY: 0.5
}));
centerMarker.x = WIDTH / 2;
centerMarker.y = 1100;
crateWinningReward = sanitizeCrateReward(weightedRewardRoll(crateRewardPool));
cratePendingReward = null;
crateSpinFinished = false;
var visualData = buildSpinVisualRewards(crateWinningReward);
crateSpinData = visualData.items;
crateSpinCards = [];
crateSpinRoot = scene.addChild(new Container());
crateSpinRoot.x = 0;
crateSpinRoot.y = 0;
crateSpinStrip = crateSpinRoot.addChild(new Container());
var spacing = 240;
var startX = WIDTH / 2 - 200;
for (var i = 0; i < crateSpinData.length; i++) {
var cardX = startX + i * spacing;
makeRewardCard(crateSpinStrip, crateSpinData[i], cardX, 1100);
crateSpinCards.push({
x: cardX,
reward: crateSpinData[i]
});
}
var winningCardX = startX + visualData.winIndex * spacing;
crateSpinStrip.x = 0;
crateSpinTargetX = WIDTH / 2 - winningCardX;
crateSpinVelocity = -85;
crateSpinning = true;
crateResultTitle = makeLabel(scene, 'Spinning...', WIDTH / 2, 1450, 42, '#facc15');
crateResultSubtitle = makeLabel(scene, 'Waiting for reward', WIDTH / 2, 1510, 26, '#FFFFFF');
crateClaimButton = scene.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5
}));
crateClaimButton.x = WIDTH / 2;
crateClaimButton.y = 1880;
crateClaimButton.alpha = 0.35;
makeLabel(scene, 'CLAIM', WIDTH / 2, 1880, 36, '#08110d');
crateBackButton = scene.addChild(LK.getAsset('buttonAlt', {
anchorX: 0.5,
anchorY: 0.5
}));
crateBackButton.x = WIDTH / 2;
crateBackButton.y = 2005;
crateBackButton.alpha = 0.35;
makeLabel(scene, 'BACK TO GARAGE', WIDTH / 2, 2005, 30, '#eff6ff');
crateClaimButton.down = function () {
if (!crateSpinFinished || !crateWinningReward) return;
if (!cratePendingReward) {
cratePendingReward = applyCrateReward(crateWinningReward);
crateResultTitle.setText(cratePendingReward.title);
crateResultSubtitle.setText(cratePendingReward.subtitle);
}
initGarage();
};
crateBackButton.down = function () {
if (!crateSpinFinished) return;
initGarage();
};
game.down = function () {};
game.update = function () {
if (currentState !== 'crate_opening') return;
if (crateSpinning) {
var delta = crateSpinTargetX - crateSpinStrip.x;
crateSpinVelocity *= 0.965;
if (Math.abs(delta) > 10) {
crateSpinStrip.x += delta * 0.06 + crateSpinVelocity;
} else {
crateSpinStrip.x += delta * 0.18;
}
if (Math.abs(delta) < 2.5 && Math.abs(crateSpinVelocity) < 0.35) {
crateSpinStrip.x = crateSpinTargetX;
crateSpinning = false;
crateSpinFinished = true;
crateResultTitle.setText(crateWinningReward.label);
crateResultSubtitle.setText('Tap CLAIM to apply reward');
crateClaimButton.alpha = 1;
crateBackButton.alpha = 1;
}
}
};
}
/****
* Start
****/
initGarage();
Fullscreen modern App Store landscape banner, 16:9, high definition, for a game titled "Build, Race, Upgrade" and with the description "A stylized drag racing game where players build and tune cars in a garage, compete in timed drag races against AI opponents, and reinvest winnings into performance upgrades. Progression spans beginner, intermediate, and advanced race tiers with increasingly difficult rivals and larger payouts. Clean modular systems enable easy balancing and future expansion.". No text on banner!