Tuning and upgrades
A car’s performance comes from two layers. Parts (bought in the shop) change the car’s
horsepower, torque and weight once, in sim/specs.ts. A tune (set in the Tuning screen) is nine
sliders that sim/initRace.ts turns into gear ratios, gear top speeds and engine heat at the
start of every race. Both are pure sim code, so the shop preview, the tune analyzer, the AI, the
online server and npm run parts:check all compute the same numbers. The race step that consumes
them is covered in Race simulation.
Parts and stages
Section titled “Parts and stages”sim/upgrades.ts holds the parts from the 2004 original’s UPGRADELIST table and the cash stages
they are bought in. The full tables are generated on Parts and stages; this section
explains how they combine.
A Part has a category, a base cost and three raw percentages: upHP, upTQ, upWT. When the
module loads, every part’s three percentages are scaled by one factor so that the upHP of all
parts sums to MAX_UPGRADE_PERCENT (150, as the original’s MAXUP). The ratios between parts stay
as in the original table; only the overall size of an upgrade is fixed.
A Stage has hp, tq and wt factors and a price multiplier on the part’s cost. Index 0 is
Stock (no effect, nothing installed), then Stage 1 to Stage 5 (TOP_STAGE): each one more
power for more money, no trade-offs (docs/2026-11-08-progression-design.md, “Parts: cash stages”).
Each stage costs twice the one before (0.5, 1, 2, 4, 8 times the part’s cost), so a car’s first
stages come fast and its last ones slowly. Buying a higher stage pays its full price, nothing back
for the stage it replaces.
They replace the original’s nine PERFMANU brands; brands are now a separate rare layer on top of a
stage (Part brands). Stage 2 keeps the old FRP brand’s factors, the stage career opponents race at
(OPPONENT_STAGE in player/career/eventSpec.ts). An index above TOP_STAGE (a save from the brand
tiers) reads as the top stage (stageAt).
| Field | Meaning |
|---|---|
single |
No stages: nitrous kit, reserves, weight stages. Installed at SINGLE_STAGE and priced at plain cost. |
requires |
Another part id that must be installed first (blow-off valves and turbo upgrade need the turbo, reserves chain on each other, weight stages chain on each other). |
Prices come from partPrice(part, stage, carClass): cost * stage.price for staged parts, cost
for single ones, both times the car’s class price (CLASS_PRICE: 3.75, 40, 90 and 120 for classes 1
to 4), so the same part costs more on a faster car, as that car’s district pays more. The shop asks
carOffers(part, car) in player/shopRules.ts, which reads the class off the car’s model.
buildCarStats: from base car to CarStats
Section titled “buildCarStats: from base car to CarStats”buildCarStats(base, upgrades, brands) in sim/specs.ts is the original’s calcSpecs. upgrades is a
map of part id to stage index, stored per owned car in the save (player/garageState.ts). brands
(optional) maps part ids to a brand and level; see Part brands.
For every installed power part (engine, ignition, fuel, exhaust, intake, computer, pulley, intercooler, weight and nitrous parts) it multiplies:
hp *= 1 + stage.hp * part.upHP / 100 * classFactortq *= 1 + stage.tq * part.upTQ / 100 * classFactorengineWeight *= 1 + stage.wt * part.upWT / 100 * classFactorThe other parts change the drag model instead and weigh nothing (sim/partEffects.ts). Their
strength is the stage’s stage.hp times classFactor, and the sum lands in CarStats.parts:
| Part | Effect |
|---|---|
dragtires |
grip +0.7 x strength: static and sliding tyre friction, so up to 70% more (56% on class 3 and 4). Traction control caps the drive force at the grip, so on a traction-limited car the tyres are what lets more power through. |
coilovers, springs |
squat +0.3 / +0.15 x strength: more of the weight transfer goes the driven axle’s way (a RWD car squats more, a FWD car lifts less). AWD already uses the whole car’s weight, so it gains nothing. |
drivetrain |
efficiency +0.04 x strength, and the gearbox by stage: stages 1 and 2 a quicker manual (0.2 s per shift), stage 3 a sequential (0.12 s), stages 4 and 5 a dual-clutch (0.05 s). Stock is 0.25 s. |
turbo, turboup, blowoff |
turboGain +0.4 / +0.15 / +0.1 x strength: the turbo’s extra torque at full spool, times the BOOST slider. It starts to spool at 45% of the redline and is full 20% later. |
CarStats.HP and TQ are the engine without its turbo; ratedHp and ratedTq in sim/specs.ts
add the turbo at BOOST 1, and that is what the garage, the parts shop and the career’s maxHp rule
show and check.
Parts never change the gearing: CarStats.finalDrive is the stock model’s (stockFinalDrive in
sim/physics/buildCar.ts), so a heavily tuned car can run into the limiter in sixth.
Consequences worth knowing:
- Parts compound. Each part multiplies the result of the ones before, so a fully built car gains more than the plain sum of the percentages.
- R-class factor.
classFactoris 0.8 whencarClassis 3 (R) or 4 (X): those cars get 80% of every upgrade, as R-class cars did in the original. The class of each car is on Cars. - Most power parts add weight. A positive
upWTmakes the engine heavier. Only parts with a negativeupWT(the weight stages) make the car lighter. Weight touches the engine weight only; the frame weight never changes. TotalWTis engine plus frame. - Single parts use the
SINGLE_STAGEstage’s factors, so a weight stage’supWTis scaled by that stage’swtlike any other part.
Nitrous is not a multiplier. nitrousStats reads the installed ids directly:
| Part | Effect on CarStats |
|---|---|
nitrouskit |
nitrous tank at 100%, otherwise 0. New cars come with it (newCar in player/garageState.ts). |
reserve1, reserve5, reserve7 |
nitrousReserves is the largest installed count (1, 5 or 7 bottles). The race refills the tank from a bottle when it runs nearly empty (sim/step.ts). Reserves also carry a small positive upWT. |
dualport |
dualPortNitrous: the nitrous shot adds twice the torque (nitrousTorque in sim/step.ts). |
The tune
Section titled “The tune”A Tune (sim/types.ts) is nine numbers, all 1 in STOCK_TUNE:
| Key | Slider | What it does in initRace and sim/engine.ts |
|---|---|---|
GEAR1 to GEAR6 |
GEAR 1 to 6 | Divides that gear’s ratio: ratio / GEARn. Higher: a longer gear, more top speed in it and less drive force. Lower: a shorter gear that pulls harder but hits the limiter sooner. The shift points follow the new ratios (sim/physics/shiftPoints.ts). |
FINALDRIVE |
FINAL | Divides the final drive, CarStats.finalDrive / FINALDRIVE: every gear longer or shorter at once. A heavily built car that hits the limiter in sixth needs it raised; a short final can still be quicker over a short race. A tune saved before this slider reads it as 1 (normalizeTune, slotTunes). |
BOOST |
BOOST | Needs a turbo: scales its turboGain, so more boost is more torque above the spool rpm and a hotter engine. Without a turbo it does nothing. |
AIRINTAKE |
AIR | Only lowers the base temperature, via (2 - AIRINTAKE). No power effect yet. |
CAMADJUST |
CAM | Moves torque between the ends of the curve (buildTorqueCurve): above 1 the low end loses up to 24% and the top end past peak power gains up to 8%, below 1 the other way round. Which way is quicker depends on the car. |
The stock gearbox is six geometric ratios from 3.4 to 0.85, with a final drive set per car model so
the stock car reaches its top speed at the redline in sixth (sim/physics/buildCar.ts). Parts never
change it; the six gear sliders and FINAL do.
Every value is clamped to TUNE_MIN and TUNE_MAX (sim/initRace.ts) by normalizeTune, which
keeps every gear ratio positive and finite.
Heat. baseTemp is (2 - AIRINTAKE) * 10 + BOOST * 40, with BOOST counted as 1 on a car
without a turbo. The engine only heats while the rev is
past the car’s redline (redlineRev) and cools back to baseTemp below it; at ENGINE_BLOW_TEMP
it blows (sim/engine.ts). So boost and air do not overheat a car on their own: they set how much
headroom the player has for over-revving or a missed shift.
Weight. WT is the car’s mass in kg in the drag model: it divides the net force, and it sets
how much load the driven tyres can push on (weight transfer included, see
Race simulation). There is no separate weight penalty any more.
Tune slots and the draft
Section titled “Tune slots and the draft”player/tuneSlots.ts gives each owned car three tunes, slots A, B and C. car.tune always mirrors
the active slot, so race code reads one field and never needs to know about slots. Saves from before
slots existed have only car.tune; it becomes the active slot and the others start at stock.
player/tuneEditor.ts (TuneEditor) is the Tuning screen’s working copy:
- Slider changes go into a draft kept in the Phaser registry (
DRAFT_KEY) for as long as it differs from the saved slot. That is why a test run can drive an unsaved tune and the edit is still there when the player comes back to Tuning. - Save writes the draft into the slot and makes it active. Picking another slot, leaving Tuning, or pressing Stock asks first when there are unsaved changes. Stock resets every slider to 1 and saves.
Test runs and ghosts
Section titled “Test runs and ghosts”The Test run button (or T) in Tuning starts race/scenes/TestRun.ts, a solo run on the plain strip
with the draft tune if there is one, else the saved tune. player/TestSession.ts runs it:
- The player’s input is recorded as an event log (gas and nitrous edges, shift presses, with the
frame they happened on), in the same format as the online input log (
sim/inputLog.ts). - The opponent is a ghost: the previous run’s event log replayed tick by tick through the same
stepRacer. AGhostRunstores its ownCarStatsandTune, so the replay is exact even after the player bought parts or changed the tune since. The difference on the strip is then purely the change. - The run ends on the finish line, a blown engine, or after
MAX_RACE_FRAMES(shown as too slow).
player/testRunStore.ts keeps, per owned car in its save: the last run (the next ghost, used only
when the next run is over the same distance), the best time, and the latest HISTORY runs. Each
history entry stores the tune slot name (with * when the run used an unsaved draft), the launch and
shift grades from gradeRun (sim/runGrades.ts), and the speed at the line. Selling a car takes its
runs with it.
The Tuning screen’s analyzer
Section titled “The Tuning screen’s analyzer”garage/view/tuneAnalyzer.ts is the right-hand panel of garage/scenes/Tuning.ts, with two tabs:
- ANALYZER reruns
simulateBestRun(sim/bestRun.ts) on every slider change: the car’s current stats with the tune being edited, over the distance picked in its tabs (garage/view/distanceTabs.ts). It shows the perfect time, the speed and gear at the line, the gear the nitrous starts in, and per gear its top speed (at the limiter) and the rpm the best run shifts at. A run that ends on the limiter in sixth adds “raise FINAL”.simulateBestRunshifts every gear where the next gear pulls harder and searches the best launch rev and nitrous gear, so this is the best time the tune allows, not what the player will drive. - RUNS (
garage/view/testRunList.ts) lists the stored history: time, gap to the best, tune slot, launch and shift grades as coloured marks, and speed at the line.
There is no separate dyno view in the code. garage/view/tuningHelp.ts holds the one-line hints
above the sliders, and garage/view/tuneSlotTabs.ts the A/B/C tabs.
The parts shop
Section titled “The parts shop”shop/scenes/PartsShop.ts lists categories, then the parts in a category, then the stages on
offer. The rules live in shop/parts/partOffers.ts, without Phaser:
offersFor: a staged part offers every stage above the installed one; a single part offers one purchase, once. A part whoserequiresis missing is locked and shows what to install first.- Buying a higher stage replaces the installed one with no refund, as in the original.
snapshotandsnapshotAfterbuild the car’s stats with and without the offer and runsimulateBestRunover the quarter mile with the car’s saved tune.shop/parts/PreviewPanel.tsshows the HP, torque and weight that change, that time, and what the part does to grip, gearbox or turbo (shop/parts/effectChanges.ts), before and after (green better, red worse). When a part makes a car whose tyres spin slower, it says to fit drag tyres first (outgrowsTyres; since traction control the best run no longer spins, so this line does not show). Hovering a stage previews it; only the selected one can be bought.shop/parts/bestValue.tspicks, among the offers the player can afford, the one with the most quarter mile time saved per dollar.shop/parts/AdviceLine.tsshows it above the shop and opens it on tap.
Buying subtracts the price from cash and sets upgrades[part.id] = stage; the next race rebuilds
CarStats from that map through currentStats (player/garageState.ts).
npm run parts:check
Section titled “npm run parts:check”scripts/parts-check.ts guards the rule that buying a part must never make a car slower. For every
car model and every part at every stage, it compares the best quarter mile time (stock tune, plus
the part’s requires when it has one, and a nitrous kit for nitrous parts only) with the same car
without the part. It tolerates a couple of frames of jitter (NOISE_SECONDS) because moving shift
frames can shift a finish by a frame. A slower part whose run spins longer is printed as spins and
allowed; since traction control (physics/traction.ts) no best run spins, so none shows. Every other slower
combination is printed as SLOWER and makes it exit 1. tests/parts.test.ts checks the same rule. Run it after
changing part data, stage factors, weight handling or anything in initRace.
npm run stages:check
Section titled “npm run stages:check”scripts/stages-check.ts guards the promise that a higher stage is simply more power: every dealer
and boss car with all staged parts at one stage must get quicker with each stage at every distance,
and a greedy search that lowers parts from full Stage 5 must find no build quicker over the quarter
mile beyond frame jitter. npm run check runs it (about 4 s). It failed before traction control (17
of 210 stage steps slower, 9 cars quicker with a lighter build); see
docs/2026-11-08-stage-traction-design.md. Quarter mile, stock tune, uniform stages, before and
after traction control:
| Car | Stock | Stage 2 | Stage 5 |
|---|---|---|---|
| Hatch C1 | 16.81 s, unchanged | 14.00 to 13.43 s | 12.35 to 10.98 s |
| Hatch Y2 | 15.50 to 15.05 s | 13.52 to 12.43 s | 12.34 to 10.37 s |
| Coupe B3 | 14.48 to 14.07 s | 12.25 to 11.15 s | 11.00 to 8.64 s |
| Super R7 | 10.41 to 10.05 s | 9.32 to 8.63 s | 9.16 to 7.31 s |
| Hyper GX | 10.05 to 9.52 s | 8.73 to 7.84 s | 8.37 to 6.56 s |