Code edit (7 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
User prompt
None of the UI buttons are working can you double check everything.
Code edit (3 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Can't find variable: launchQuality' in or related to this line: 'var launchBonus = launchQuality === 'perfect' ? 300 : launchQuality === 'good' ? 150 : 50;' Line Number: 249
Code edit (1 edits merged)
Please save this source code
User prompt
Take the vehicle customization + drag racing game concept we just defined and now turn it into a complete vertical slice implementation plan and code structure. I want a playable core loop: 1. Player starts in a garage 2. Player can view car stats and buy/install upgrades 3. Installed upgrades change the car’s performance stats 4. Player enters a drag race against 1 AI opponent 5. Race result calculates payout based on win/loss, finish time, reaction time, launch quality, and top speed 6. Player earns cash 7. Player returns to garage to upgrade again and repeat the loop Requirements: - Make this modular and production-ready - Separate systems into clear scripts/managers - Use data-driven design for cars, upgrades, races, and rewards - Include a save/load system for cash, owned upgrades, installed parts, and progression - Include a basic UI flow for Garage, Shop, Race HUD, and Results Screen - Include at least one starter car and one higher-tier opponent - Include upgrade categories: engine, turbo, tires, transmission, weight reduction, and visuals - Each upgrade must affect real car stats - AI opponent should scale by race tier - Keep the first version simple but expandable Output format: - First give me the full system architecture - Then list the exact scripts/classes to create - Then explain the order they should be built in - Then give starter code for the most important core systems first - Use clean naming and avoid overly complex features in version 1 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
Code edit (1 edits merged)
Please save this source code
User prompt
Build, Race, Upgrade
Initial prompt
Create a stylized vehicle customization and drag racing game with a satisfying repeatable gameplay loop. The player starts in a garage where they can buy and install car parts such as engine upgrades, turbo, tires, transmission, suspension, weight reduction, paint, wheels, and body kits. After customizing the car, the player enters a drag race event, competes in a short straight-line race, and earns cash based on performance including win/loss, finish time, reaction time, top speed, and launch quality. That cash is then used to upgrade the car and enter harder races, repeating the loop. Build the game with clean modular systems for Garage, Car Stats, Drag Race Manager, Reward System, Upgrade Shop, and Save/Load. Include a simple but polished UI, clear progression, increasingly difficult AI racers, and data-driven tuning so cars and upgrades are easy to balance. Build a game called “Build, Race, Upgrade” focused on car progression through drag racing. The core loop is: enter garage, customize and tune car, start drag race, earn money from race results, return to garage, buy better upgrades, and repeat. I want the garage to feel rewarding, with visible stat changes for horsepower, torque, grip, weight, and gearing whenever parts are installed. The race should emphasize launch timing, gear shifts, acceleration, and top-end speed rather than open-world driving. Include beginner, intermediate, and advanced race tiers, each with larger rewards and tougher opponents. Create systems for economy balancing, upgrade unlocks, car stat calculation, opponent scaling, and persistent save data. Make the code structure clean and expandable so more cars, tracks, parts, and race modes can be added later. Design a full arcade drag racing game where customization directly affects race performance and earnings. The player begins with a basic starter car and limited cash. In the garage they can modify visuals and performance, including paint color, rims, ride height, spoilers, engine internals, ECU tune, turbo, nitrous, gearbox, and tire compound. Each installed part should update the car’s stats in real time and change how the car performs in drag races. Races should be short, intense, and replayable, with a countdown, launch window, shifting mechanic, speed readout, opponent AI, finish results, and cash payout. Payouts should scale using placement, race class, clean shifting, reaction time, and personal best performance. After each race, the player returns to the garage to spend winnings and improve the car. Create the game so this loop feels addictive, polished, and easy to expand with more content later. Generate a car-building progression game with a strong “garage to race to upgrade” gameplay loop. The experience starts in a customization garage where the player can inspect their car, install upgrades, see part costs, compare old vs new stats, and preview visual changes. Once ready, the player enters a drag strip event where the gameplay focuses on launch timing, traction, acceleration, gear shifting, and beating an AI rival to the finish. When the race ends, award money based on race outcome and performance metrics, then send the player back to the garage. Include an upgrade tree or shop system that gradually unlocks better parts and encourages strategic choices between speed, grip, reliability, and tuning. Build a complete vertical slice with one garage, one starter car, multiple upgrades, several AI racers, and a satisfying progression loop that makes players want to race again immediately. Create a polished prototype for a vehicle customization drag racer with progression, tuning, and replayability. The player loop should be: buy or modify parts in garage, tune the car for drag performance, enter a race, try to get the best launch and shift timing, finish the race, earn cash rewards, unlock better parts, and repeat with faster opponents. Include systems for part categories, stat calculation, race payouts, difficulty progression, basic tuning adjustments, and a simple save system. The garage should clearly show current cash, installed parts, car stats, and upgrade options. The drag race should feel responsive and arcade-friendly, with visible countdown lights, RPM control before launch, gear changes during the run, and a post-race summary screen. Write the project in a scalable way with separate managers and ScriptableObject-style data structures so the game can grow into a larger car-building drag racing title.
/****
* 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(); ===================================================================
--- original.js
+++ change.js
@@ -3,19 +3,26 @@
****/
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,
- wheelLevel: 0,
- bodyKitLevel: 0,
- bestTime: 0,
- raceCount: 0
+ challengeRaceCount: 0,
+ challengeWins: 0,
+ challengePerfectLaunches: 0,
+ currentEventId: "street_sprint",
+ ladderStage: 0
});
/****
* Initialize Game
@@ -29,8 +36,109 @@
/****
* 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';
@@ -41,8 +149,9 @@
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;
@@ -61,8 +170,25 @@
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,
@@ -92,8 +218,58 @@
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() {
@@ -120,28 +296,22 @@
var transmission = getUpgradeLevel('transmission');
var suspension = getUpgradeLevel('suspension');
var weight = getUpgradeLevel('weight');
var tire = getUpgradeLevel('tire');
- var hp = 160 + engine * 28 + turbo * 36;
- var torque = 210 + engine * 22 + turbo * 30;
- var grip = 72 + tire * 6 + suspension * 4;
- var mass = 1450 - weight * 55;
- var shiftSpeed = 1 + transmission * 0.08;
- var traction = 1 + tire * 0.06 + suspension * 0.03;
return {
- hp: hp,
- torque: torque,
- grip: grip,
- mass: Math.max(980, mass),
- shiftSpeed: shiftSpeed,
- traction: traction
+ 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,
- torque: 220 + tier * 28,
+ 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
@@ -167,40 +337,20 @@
anchorX: 0.5,
anchorY: 0.5
}));
windowObj.y = -10;
- var wheel1 = c.addChild(LK.getAsset('wheel', {
- anchorX: 0.5,
- anchorY: 0.5
- }));
- var wheel2 = c.addChild(LK.getAsset('wheel', {
- anchorX: 0.5,
- anchorY: 0.5
- }));
- var wheel3 = c.addChild(LK.getAsset('wheel', {
- anchorX: 0.5,
- anchorY: 0.5
- }));
- var wheel4 = c.addChild(LK.getAsset('wheel', {
- anchorX: 0.5,
- anchorY: 0.5
- }));
- var rim1 = c.addChild(LK.getAsset('rim', {
- anchorX: 0.5,
- anchorY: 0.5
- }));
- var rim2 = c.addChild(LK.getAsset('rim', {
- anchorX: 0.5,
- anchorY: 0.5
- }));
- var rim3 = c.addChild(LK.getAsset('rim', {
- anchorX: 0.5,
- anchorY: 0.5
- }));
- var rim4 = c.addChild(LK.getAsset('rim', {
- anchorX: 0.5,
- anchorY: 0.5
- }));
+ 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
}, {
@@ -212,27 +362,46 @@
}, {
x: 45,
y: 36
}];
- var wheels = [wheel1, wheel2, wheel3, wheel4];
- var rims = [rim1, rim2, rim3, rim4];
- for (var i = 0; i < 4; i++) {
- wheels[i].x = positions[i].x;
- wheels[i].y = positions[i].y;
- rims[i].x = positions[i].x;
- rims[i].y = positions[i].y;
+ 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) {
- rim1.rotation += speed;
- rim2.rotation += speed;
- rim3.rotation += speed;
- rim4.rotation += 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;
@@ -250,9 +419,271 @@
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() {
@@ -261,57 +692,113 @@
LK.playMusic('garageMusic', {
loop: true
});
scene = game.addChild(new Container());
- makeLabel(scene, 'MIDNIGHT STRIP', WIDTH / 2, 120, 84, '#FFFFFF');
- makeLabel(scene, 'Build • Launch • Shift • Upgrade', WIDTH / 2, 205, 34, '#93c5fd');
- var cashPanel = scene.addChild(LK.getAsset('panel', {
+ 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 = 340;
- makeLabel(scene, 'Cash: ' + formatCash(storage.playerCash || 0), WIDTH / 2, 340, 42, '#22c55e');
- makeLabel(scene, 'Tier: ' + ((storage.playerTier || 0) + 1), WIDTH / 2, 405, 28, '#facc15');
+ 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 = WIDTH / 2;
- playerCar.y = 730;
+ playerCar.x = 270;
+ playerCar.y = 700;
+ playerCar.scale.set(1.65);
playerCar.setColor(storage.carColor || 0xdc2626);
- playerCar.scale.set(1.55);
var stats = getPlayerStats();
- makeLabel(scene, 'HP: ' + stats.hp, 250, 1020, 34, '#FFFFFF', 0, 0.5);
- makeLabel(scene, 'Torque: ' + stats.torque, 250, 1075, 34, '#FFFFFF', 0, 0.5);
- makeLabel(scene, 'Grip: ' + stats.grip, 250, 1130, 34, '#FFFFFF', 0, 0.5);
- makeLabel(scene, 'Weight: ' + stats.mass + 'kg', 250, 1185, 34, '#FFFFFF', 0, 0.5);
- makeLabel(scene, 'Shift Speed: ' + stats.shiftSpeed.toFixed(2), 250, 1240, 34, '#FFFFFF', 0, 0.5);
- makeLabel(scene, 'UPGRADES', WIDTH / 2, 1360, 46, '#facc15');
+ 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 card = scene.addChild(LK.getAsset('upgradeCard', {
+ var card3 = scene.addChild(LK.getAsset('upgradeCard', {
anchorX: 0.5,
anchorY: 0.5
}));
- card.x = 560 + index % 2 * 930;
- card.y = 1510 + Math.floor(index / 2) * 150;
- makeLabel(scene, data.label + ' Lv.' + level, card.x, card.y - 22, 30, '#FFFFFF');
- makeLabel(scene, level >= data.maxLevel ? 'MAXED' : 'Buy: ' + formatCash(cost), card.x, card.y + 20, 24, level >= data.maxLevel ? '#facc15' : '#22c55e');
- card.down = function () {
+ 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 RACE', WIDTH / 2, 2400, 40, '#08110d');
+ 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;
@@ -322,17 +809,20 @@
LK.getSound('cashReward').play();
initGarage();
}
/****
-* Countdown / launch
+* 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
@@ -351,33 +841,36 @@
playerCar.setColor(storage.carColor || 0xdc2626);
opponentCar = scene.addChild(createCar(true));
opponentCar.x = 340;
opponentCar.y = 1620;
- makeLabel(scene, 'LAUNCH ON GREEN', WIDTH / 2, 180, 58, '#FFFFFF');
- makeLabel(scene, 'Tap too early = weak launch. Tap on green with strong RPM = best launch.', WIDTH / 2, 255, 28, '#facc15');
- var lightX = WIDTH / 2;
- var startY = 520;
+ 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 = lightX;
- light.y = startY + i * 72;
+ 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, 'Hold steady and tap on GREEN', WIDTH / 2, 1000, 34, '#FFFFFF');
+ 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;
+ 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';
@@ -401,8 +894,9 @@
launchLights[countdownIndex].y = 520 + countdownIndex * 72;
countdownIndex++;
}
if (countdownIndex >= 3 && !playerLaunched && countdownElapsed >= 0.40) {
+ foulStart = false;
launchQuality = 'LATE';
launchAccuracy = 0.2;
playerLaunched = true;
beginRace();
@@ -419,119 +913,386 @@
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 in the sweet spot');
+ promptText.setText('Tap to shift through the gears');
game.down = function () {
if (currentState !== 'race' || !raceStarted || raceFinished) return;
- if (playerCanShift) {
+ 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++;
- playerCanShift = false;
- playerRPM = 4200 - getUpgradeLevel('transmission') * 120;
+ playerRPM = 3400;
+ playerSpeed *= 0.96;
+ promptText.setText('EARLY SHIFT');
LK.getSound('shiftGear').play();
- promptText.setText('GOOD SHIFT');
+ } 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 playerLaunchBoost = 0.75 + launchAccuracy * 0.65;
- var playerAccel = playerStats.torque / playerStats.mass * 2.4 * playerLaunchBoost / Math.max(1, playerGear * 0.82);
- var aiLaunchBoost = 0.92 + (storage.playerTier || 0) * 0.03;
- var aiAccel = aiStats.torque / aiStats.mass * 2.15 * aiLaunchBoost / Math.max(1, opponentGear * 0.84);
- playerRPM += 95 + playerGear * 24 + getUpgradeLevel('engine') * 6;
+ 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');
- } else if (playerRPM > playerShiftWindowMax + 350) {
- playerCanShift = false;
- promptText.setText('LATE SHIFT');
}
- if (playerRPM > 7600) {
- playerRPM = 5200;
- playerSpeed *= 0.985;
+ if (playerRPM > playerShiftWindowMax + 650) {
+ playerRPM = 6850;
+ playerSpeed *= 0.992;
+ promptText.setText('ON LIMITER');
}
- if (playerGear < 6) {
+ if (playerGear <= playerGearMax) {
playerSpeed += playerAccel;
- } else {
- playerSpeed += playerAccel * 0.5;
}
- if (opponentGear < 6 && Math.random() < 0.025 + (storage.playerTier || 0) * 0.005) {
+ 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 * 0.42);
- opponentCar.x = 340 + Math.min(1320, opponentDistance * 0.42);
+ 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 >= 3000) {
- playerFinishTime = raceTimer;
- }
- if (!opponentFinishTime && opponentDistance >= 3000) {
- opponentFinishTime = raceTimer;
- }
+ if (!playerFinishTime && playerDistance >= currentRaceDistance) playerFinishTime = raceTimer;
+ if (!opponentFinishTime && opponentDistance >= currentRaceDistance) opponentFinishTime = raceTimer;
if (playerFinishTime && opponentFinishTime) {
raceFinished = true;
- showResults();
+ processRaceResults();
}
};
}
/****
-* Results
+* Results processing
****/
-function showResults() {
- currentState = 'results';
+function processRaceResults() {
+ var eventDef = getCurrentEvent();
var playerWon = playerFinishTime <= opponentFinishTime;
- var tier = storage.playerTier || 0;
- var baseReward = playerWon ? 900 + tier * 250 : 300;
- var launchReward = launchQuality === 'PERFECT' ? 300 : launchQuality === 'GOOD' ? 180 : launchQuality === 'OK' ? 100 : launchQuality === 'POOR' ? 40 : 0;
- var timeBonus = Math.max(0, Math.floor((12 - playerFinishTime) * 80));
- var reward = baseReward + launchReward + timeBonus;
- storage.playerCash = (storage.playerCash || 0) + reward;
- storage.raceCount = (storage.raceCount || 0) + 1;
- if (playerWon && tier < 5) {
- storage.playerTier = tier + 1;
+ 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());
- makeLabel(scene, playerWon ? 'YOU WIN' : 'YOU LOSE', WIDTH / 2, 180, 86, playerWon ? '#22c55e' : '#ef4444');
- makeLabel(scene, 'Launch: ' + launchQuality, WIDTH / 2, 420, 42, '#facc15');
- makeLabel(scene, 'Your Time: ' + playerFinishTime.toFixed(2) + 's', WIDTH / 2, 520, 40, '#FFFFFF');
- makeLabel(scene, 'Opponent Time: ' + opponentFinishTime.toFixed(2) + 's', WIDTH / 2, 600, 40, '#FFFFFF');
- makeLabel(scene, 'Reward: ' + formatCash(reward), WIDTH / 2, 720, 48, '#22c55e');
- makeLabel(scene, 'Best Time: ' + (storage.bestTime ? storage.bestTime.toFixed(2) + 's' : '--'), WIDTH / 2, 810, 34, '#93c5fd');
- makeLabel(scene, 'Cash Total: ' + formatCash(storage.playerCash || 0), WIDTH / 2, 880, 34, '#FFFFFF');
+ 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 = 1300;
- carPreview.scale.set(1.7);
+ 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 = 2200;
- makeLabel(scene, 'BACK TO GARAGE', WIDTH / 2, 2200, 38, '#08110d');
+ 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 () {};
+ 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
****/
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!