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/partList.ts holds the parts and sim/upgrades.ts the cash stages they are bought in and the
rules that tie them together. Every part is a real part with roughly its real effect (CLAUDE.md,
“Plausible to a car person”); the ranges and sources are in
docs/2026-11-10-real-parts-and-cash-design.md, “Reference”. The full tables are generated on
Parts and stages; this section explains how they combine.
A Part has a category, a cost (its Stage 1 price on a class 1 car, in dollars) and an effect:
the shares it changes at Stage 5. A Stage has a strength (the share of that full effect: 0.4,
0.5, 0.65, 0.8 and 1 for Stage 1 to 5) and a price multiplier on the cost (1, 1.6, 2.5, 4 and 6).
Index 0 is Stock; each stage is a better version of the same part (a short shifter, then a
sequential, then a dual-clutch), more effect for more money, no trade-offs. Buying a higher stage
pays its full price, nothing back for the stage it replaces. An index above TOP_STAGE (a save from
the old brand tiers) reads as the top stage (stageAt); a part id the list no longer has is ignored.
| Field | Meaning |
|---|---|
single |
No stages: the nitrous kits and bottles and the weight parts. Stored at SINGLE_STAGE, full effect, priced at plain cost. |
requires |
At least one of these must be installed first (the boost add-ons need the turbo or the supercharger, bottles chain on each other). |
excludes |
Cannot sit next to this part: the turbo and the supercharger share one boost slot. Buying one replaces the other, and its brand moves over (player/installPart.ts). |
gatedBy |
Support parts: this part’s stage can be at most GATE_LEAD (2) above each of theirs. Boost needs the fuel system and forged internals, camshafts and the stroker kit need forged internals. The shop shows the stages above that as locked, with what they need. |
notOn |
The effect does nothing on this drive layout (the limited-slip differential on all-wheel drive). |
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: 1, 2, 4 and 6 for classes 1 to 4),
as real parts for a faster car cost more. A full Stage 3 build costs about a class 1 car; Stage 5 is
a race car. 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 turns a car model and its installed parts
into CarStats. 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.
First activeParts drops what does not count: unknown ids, parts whose requirement is missing, the
second boost kind, and clamps every gated part to the stage its support parts allow (opponents’ builds
can list more). Then each part adds its effect at strength = stage.strength * classFactor
(sim/partEffects.ts):
| Effect | What it changes |
|---|---|
hp |
The whole curve: power and torque both times 1 + hp * strength, as bolt-ons lift both. |
tq |
Extra torque on top (the stroker kit’s displacement). |
lowEnd, revRange |
The torque at idle, and the redline and limiter, which add that share of power up there (physics/torqueCurve.ts). Camshafts and the intake manifold trade a little low end for top end. |
turboGain, superGain |
The turbo’s or the supercharger’s extra torque at full boost. The turbo spools from 45% of the redline to full 20% later; the twin-screw supercharger rises from idle to full at 30% of the redline (turboBoost in physics/stepPhysics.ts). The BOOST slider scales both. |
boostScale |
A share added to whatever boost is fitted: the ECU tune (+25% at Stage 5), intercooler (+10%), boost controller (+20%), water-methanol injection (+10%). |
grip, squat |
Tyre friction (drag tyres up to +70%, the differential +8%) and the share of weight transfer going the driven axle’s way (drag suspension, traction bars; AWD gains nothing). |
mass |
A share off the car’s mass: lightweight wheels and the single weight parts (interior strip, carbon hood, lexan windows, carbon doors and trunk). |
The gearbox and clutch part sets the shift time by stage: 0.2 s at stages 1 and 2, a sequential’s
0.12 s at stage 3, a dual-clutch’s 0.05 s at 4 and 5 (stock 0.25 s). The sequential and the
dual-clutch are whole new boxes: stage 3 fits a 6-speed and stages 4 and 5 a 7-speed
(PartEffects.minGears), so a car with fewer stock gears gains them; it never loses any.
CarStats.HP and TQ are the engine without its boost; ratedHp and ratedTq in sim/specs.ts
add the rev range and the boost at BOOST 1, and that is what the garage, the parts shop and the
career’s maxHp rule show and check. WT is the model’s mass (real kerb weight with the driver)
less the weight parts’ and the brand’s shares.
Parts change the gearing only through the gearbox: CarStats.finalDrive is the model’s
(stockFinalDrive in sim/physics/buildCar.ts) for the box it has, so a heavily tuned car can run
into the limiter in its top gear.
Consequences worth knowing:
- Parts compound. Each part multiplies the result of the ones before.
- Class factor.
classFactoris the class’s share of every upgrade,CLASS_FACTOR0.84, 0.78, 0.6 and 0.62 for classes 1 to 4, sized so every class’s full builds land in its band of quarter mile times and trap speeds (npm run bands:check,scripts/bands-check.ts; part ofnpm run check). The class of each car is on Cars. - No part adds weight, and support parts add nothing alone: they unlock the stages above.
Nitrous is not a multiplier. nitrousStats reads the installed ids directly:
| Part | Effect on CarStats |
|---|---|
nitrouskit (Wet Nitrous Kit) |
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, 2 or 3 bottles). The race refills the tank from a bottle when it runs nearly empty (sim/step.ts). |
dualport (Direct-Port Nitrous) |
dualPortNitrous: the shot adds 300 hp instead of the wet kit’s 150 (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 GEAR7 |
GEAR 1 to 7 | 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 its top gear 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 or supercharger: scales its turboGain or superGain, so more boost is more torque and a hotter engine. Without one it does nothing, and Tuning hides the slider. |
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. |
Each car model has its own gearbox, 4 to 7 speeds (PhysicsSpec.gears in player/carModels.ts),
each a real gearbox’s ratios (sim/physics/gearboxes.ts), with a final drive set so the stock car
reaches its top speed at the redline in its top gear. A model can set its own final drive instead:
the 4-speed Ironside Haymaker runs a 3.73 rear axle, as 60s muscle cars did. The Tuning screen shows
a gear slider per gear the car has, and BOOST only with a turbo or supercharger (tunePanel in garage/view/tuneSliders.ts); the tune keeps
the other gear values and the race ignores them.
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, per race distance: nine in
all, in car.tunes. One slot per distance is lit (car.litTunes), and a race reads the lit tune of
its own distance with tuneFor(car, distance): a career event, a boss round, a bracket dial-in, a
test run. Gearing that wins an eighth rarely wins a half, and this way the player never has to
remember which slot holds which distance. An empty slot reads as its distance’s lit tune, so trying a
variant starts from what races; a distance with nothing saved races stock. car.tune holds the tune
last saved or picked, for what shows one tune without a distance: the dyno, a friend’s test drive.
Picking a slot on the boss screen or between tournament rounds lights it for that round’s distance,
so Tuning shows the same pick afterwards. An online room’s distance is set after joining, so the
entry brings every distance’s lit tune and the server races the room’s one (net/entryTune.ts).
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. - The distance tabs (
garage/view/distanceTabs.ts, top right) pick which distance’s slots are on the sliders; the choice holds for the session (tunedDistance). - Save writes the draft into the slot and lights it. Picking another slot or distance, leaving Tuning, or pressing Stock asks first when there are unsaved changes. Stock resets every slider to 1 and saves.
- The Friends button (
garage/view/friendTunes.ts) sends the tune on the sliders to a friend, or loads one a friend sent onto the sliders of the slot on screen, as a draft to Save or discard (Friends).
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). Past the line both cars lift and coast on behind the end panel, as in a race; the run’s log stops at the line. - The end panel is a card (
race/view/testRunCard.ts) with the same tabs as the race results (race/view/resultTabs.ts): the time against the last run and the best with the launch and shift grades, then the timeslip, speed, grip and gap to the perfect run.race/testRunTelemetry.tsreplays the run, the last run (as the second car, when it was over the same distance) and the perfect run for them. A RUNS tab lists the car’s saved runs (race/view/testRunList.ts). race/testRunPlan.tsdecides what the scene drives: the player’s car or a borrowed one against the last run, or a saved run played back (TestRunData.watch). A play-back feeds the run’s own log toTestSessionas the player’s input, races the run saved before it as the ghost, ignores the keys, saves nothing and ends on that run’s card.- From the Friends screen or a leaderboard entry, the same scene test-drives another racer’s car (
race/testCar.ts: their parts, look and active tune) against the ghost of the player’s own last run. Nothing is saved, and the end panel compares with the player’s own last run and best.
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), the speed at the line, and the run itself
(GhostRun). Tapping a row in a run list (the Tuning screen’s RUNS tab, or the RUNS tab on a test
run’s card) opens race/scenes/TestRunReview.ts: that run’s card and tabs again, against the run saved
before it, and a Watch button that plays it back on the strip.
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 that tune’s distance. It shows the perfect time with how much the sliders change it against the saved tune (garage/view/timeDelta.ts, green when quicker), 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 the top gear adds “raise FINAL” (sim/endsOnLimiter.ts), and the first time it does, after the first Tuning tips, Raya plays her gear lesson (gears-first,garage/view/gearsLesson.ts).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.
garage/view/tuningHelp.ts holds the hint above the sliders: what the slider last moved does, in plain words (“Drag a slider to see what it does.” before any), and
garage/view/tuneSlotTabs.ts the A/B/C tabs, garage/view/tuningButtons.ts the bottom row and its keys. The Friends button sits at the analyzer’s foot.
The first visit plays Raya’s walkthrough (tuning-first): each line rings the part of the screen it is about (garage/view/tuningTour.ts, through the guide panel’s focus, ui/guide/guideFocus.ts). On the starter car her pink mark sits at 0.90 on FINAL on the eighth and quarter tunes (garage/view/starterHint.ts), 0.08 s and 0.04 s quicker in a perfect run. The first Save adds a short tip on the A/B/C slots (slots-first).
The dyno
Section titled “The dyno”garage/scenes/Dyno.ts draws the current car’s power (hp) and torque (lb-ft) at the wheels
against rpm, on its active tune, so CAM and BOOST show as well as parts. dynoPull (sim/dyno.ts)
reads the race’s own torque curve (physics/torqueCurve.ts) times the turbo’s spool (turboBoost in
physics/stepPhysics.ts) times the drivetrain’s efficiency every 50 rpm from 2000 rpm to the
limiter, without nitrous. Drag racers read a chassis dyno, so it shows wheel hp (“whp”): about 15%
under the rated HP (20% with AWD, less with the drivetrain part; EFFICIENCY in
physics/buildCar.ts). The stats panel, dealer and parts shop show the rated (crank) HP. Real dyno
pulls start there too, and a race never runs lower: the launch holds 2500 rpm or more. Both lines share
one scale, as on a real dyno sheet, and the band from the redline to the limiter is shaded.
With metric units (core/unitSettings.ts) torque reads in Nm; power stays hp, as dyno shops say it.
It always compares two pulls, the second drawn faint:
- From the Garage’s Dyno button (above the stats panel): the build against the previous pull, as
a dyno shop overlays the run before the mods.
player/dynoHistory.tskeeps each car’s last and previous build (dynoRuns, synced like test runs); only a change that moves the curve makes a new pull, so tyres or gearing bought in between keep the previous one. Before the first change, and with the vs Stock button, it compares against the stock car on the stock tune. - From the parts shop’s Dyno button (
shop/parts/dynoLink.ts): the selected offer against the car as it is now. Back returns to the shop with that offer open again.
The side panel (garage/view/dynoReadout.ts) gives each peak, its rpm and the change against the
faint pull. A part that leaves the curve alone (tyres, nitrous) says so.
Before the drivetrain loss, the curve’s peaks are the rated HP and torque the stats panel and shop show
(physics/torqueCurve.ts): torque rises to a rounded peak and falls ever steeper until the power
peaks at the rated HP, then the power drops to about 92% of its peak at the redline and 85% at the
limiter. A cam above 1 lifts that top end, so
a tuned car can show a little more than its rating there.
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 that its support parts allow; a single part offers one purchase, once. A part whoserequiresis missing is locked and shows what to install first. Gated stages show as locked with the support stage they need (gatedStages,shop/parts/offerRows.ts).- 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 boost and rev range (shop/parts/effectChanges.ts), before and after (green better, red worse). A power part on a car that traction control holds back for over half a second says so: “Grip-limited for 1.4 s: tyres or suspension first” (gripLimitedLine, from the best run’sgripFrames). The other boost kind says which part it replaces. Hovering a stage previews it; only the selected one can be bought.- The shop names no best buy: a “best value” pick (most time per dollar) was removed on 2026-11-13,
as following it made the career too easy (
docs/2026-11-13-economy-rework-plan.md).
Buying subtracts the price from cash and installs the part (installPart); 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, with the
part’s requirement and its support parts at the top stage, 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 combination is printed as
SLOWER and makes it exit 1; a support part that adds nothing alone passes. Run it after changing
part data, stage strengths, 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. With the real parts and the class bands (2026-11-10) all 210 steps are quicker and no build beats
full Stage 5 on any car. Quarter mile, stock tune, uniform stages:
| Car | Stock | Stage 2 | Stage 5 |
|---|---|---|---|
| Tsubame Kogarashi | 15.88 s | 13.46 s | 11.28 s |
| Brennwerk Funke | 15.44 s | 13.19 s | 11.07 s |
| Brennwerk Strasse | 14.41 s | 12.16 s | 9.95 s |
| Brennwerk Falke | 10.87 s | 9.66 s | 8.37 s |
| Halden Fenrir | 10.25 s | 8.94 s | 7.61 s |
npm run bands:check
Section titled “npm run bands:check”scripts/bands-check.ts holds every class to its band (docs/2026-11-10-endless-career-design.md,
“Class bands”): each dealer and boss car’s quarter mile stock and fully built (every staged part at
Stage 5 plus the weight parts, no nitrous), and the full build’s trap speed, must fall inside its
class’s CLASS_BANDS, and the slowest built car of a class must beat the quickest built car of the
class below. npm run check runs it (under a second).