Career
The career is the single-player progression: four districts, each with a crew of racers who hold the events and a boss at the end. Its content is plain data in player/career/ (no Phaser), its rules are small pure functions next to it, and the screens that show and start it live in the hub/ feature module. Design intent is in docs/2026-11-06-career-design.md; this page describes what the code does. For every district, event and boss with its numbers, see the career reference.
Structure
Section titled “Structure”| Level | Type (player/career/types.ts) |
What it holds |
|---|---|---|
| District | District |
name, track, fits (the dealer models of its class), carClass (the one class it races), crew, boss |
| Crew member | CrewMember |
a name and a list of events |
| Event | CareerEvent |
stable id (saves key stars on it), mode, opponent, distance, prize, plus mode settings (rule, entrants) |
| Boss | Boss |
id, name, portrait texture key, opponent, prize |
DISTRICTS in player/career/career.ts lists them in unlock order (Street, Harbor, Chinatown, Strip). Each district is one file in player/career/districts/, written with the one-line helpers from player/career/eventSpec.ts: headToHead, restricted, bracket, wager, roll, tournament and bossOpponent. A car is written as a shorthand tuple [modelId, parts, paint], so a district file reads as a table of races.
career.ts also flattens everything into CAREER_RACES: one CareerRace per event and per boss, carrying what the race scenes need (opponent, distance, prize, track, boss flag, the event for its mode, and the rule of cars it lets in). findCareerRace(id) is how every other part of the game turns a race id back into a race.
One class per district
Section titled “One class per district”District N races class N, in every event and in the boss fight (docs/2026-11-08-progression-design.md, “Classes”), so a higher class bought early does not help in an earlier district. career.ts puts { carClass: district.carClass } into every CareerRace’s rule, on top of a restricted event’s own rule, and everything that checks a car reads that one rule: the hub before a race (hub/startRace.ts, which offers an owned car of the class or the dealer through hub/pickEligibleCar.ts), the race server’s ticket (racedCar in race/judge/racedCar.ts) and the balance check. The district tabs show the class (hub/view/districtTabs.ts). The Strip races class 4 (H, Hyper; the classes are S Street, T Tuner, P Pro and H Hyper, player/carClass.ts): the Halden Nocturne at the dealer and the Halden Vesper, Victor Lau’s pink slip.
District rules (after the Strip)
Section titled “District rules (after the Strip)”From district 5 on a district races one class and one rule on what the car is (rule on District,
a car fact rule, see Car fact rules), and the class no longer climbs
(docs/2026-11-10-endless-career-design.md, “Cars: decided”). raceRule in career.ts adds the
district’s rule to every race except those of the first OPEN_CREW crew members (isOpenEntry): they
race the class alone, so any car of the class gets in and their prizes pay toward the district’s rule
car. The district tab shows class and rule; the open crew member’s name gets “ANY TUNER CAR” beside it
(hub/view/openEntryTag.ts). The four Act 1 districts have no rule.
What a release brings, as decided: a new district every 4 weeks (every 2 when the work allows), each with two new cars, a dealer car built for its rule (priced a little above the open crew’s first-win prizes) and the boss’s pink slip, inside the class band. Its rivals sit at the top of the band for the rule and expect high brand levels, and the boss is tuned against the best legal build, so a fully built old car may enter but does not win in a day. Which class a release serves and whether it brings a second dealer car are decided later, with players. There is no selling: old cars stay in the garage, because rules call them back.
districts/testDistrict.ts is a throwaway class 2, all-wheel drive district that tries this before
district 5 exists. Only a dev build with ?testdistrict in the URL lists it (career.ts); the race
server does not know it, so its races start no ticket. It goes when district 5 is added.
Veteran bonus
Section titled “Veteran bonus”When a boss win opens a district with a rule, recordOpened (player/career/veteran.ts) stores in
CareerProgress.opened when it opened and which cars the player owned then that pass its rule (the
boss’s pink slip, just won, does not count). For VETERAN_DAYS (14) after that, a paid race in that
district raced in one of those cars earns VETERAN_SHARE (25%) more cash (payVeteran), shown on the
results screen as “(veteran +$…)”. Both numbers are estimates. The race server applies both when it
settles a single race (race/judge/settleRace.ts) or a boss fight or tournament
(race/judge/seriesRound.ts), so the clock is the server’s. Act 1 districts have no rule, so no bonus.
Opponents
Section titled “Opponents”An OpponentSpec is a driver name, a car spec and a sloppiness. player/career/opponent.ts builds it into a raceable Opponent:
- Car. The model with its first
partsperformance parts inBUILD_ORDER(opponent.ts: drag tyres first, then the power parts, then suspension, the gearbox, and the fuel system, forged internals, turbo and its add-ons last, like a street racer’s build; opponents never fit the supercharger) all at onestage. Events useOPPONENT_STAGEfromeventSpec.tsunless wrapped inatStage(Harbor’s later rivals race Stage 3);FULL_PARTSthere is the count of a full build’s parts. A part whose requirement is not in the build does nothing, and a gated part races at the stage its support parts allow (buildCarStats).detunedputs a rival’s engine below full power (the stock Hagane Oni rivals in Chinatown). Opponents always get a full nitrous tank. - Sloppiness. 0..1, how slow the AI driver reacts at green (about 1 s at 1) and how many launch and shift mistakes it makes (
sim/aiDriver.ts). District files lower it crew member by crew member, so later racers drive cleaner as well as faster. - Looks. The spec’s paint is only the pink slip’s. In the race (
race/EventRace.ts)rivalLook(player/career/rivalLook.ts) rolls a named paint, a neon underglow and a light touch of decals (a third run clean, most others get a thin white or black stripe, some with a small sponsor sticker, and a few only a sticker), anew each race so a retry can look different; the back-lane rivals get one too. A boss keeps his own paint and runs no decals; a quick race rival keeps its level’s paint. Display only: the simulation and the race server never see it. - Bosses.
bossOpponentsetsBOSS_SLOPPINESS(near zero) andprecise: true, which selects the precise driver profile insim/aiDriver.ts. A boss may also have his own stage (bossOpponent’s third argument) or apowershare (of his car’s HP and torque). Dex Moreno, Mara Kane and Victor Lau drive a car of the next district’s class, their pink slip, never above full power: Dex races it with his first 6 parts at Stage 2 (since traction control a built class 1 car hooks up as well as his all-wheel drive), Mara with 12 parts at Stage 2, Victor with 11 parts at Stage 2 (2026-11-10,docs/2026-11-10-real-parts-and-cash-design.md). Victor is a wall for Mara’s Ironside Anvil even fully built (its drag-car body tops out slower than a built Hagane Oni), so Chinatown asks for a Hagane Oni or Brennwerk Falke. The Ghost races his Halden Fenrir with 20 parts at Stage 2, between a fully built Halden Vesper and a well built Halden Nocturne. Since nitrous became a fixed shot (2026-11-11) a built car no longer makes up a second on its bottles, so Mara races at 93% power (since the bogged launch fix, so a fully built Ironside Quarter just takes her), Victor at 82% and The Ghost at 91%, and the prizes of Harbor, Chinatown and the Strip rose by 40% to keep the career at about 7.5 weeks (since 2026-11-12 it is tuned to the ramp under “Balance check”).
Difficulty is therefore tuned with three knobs per race: the model, how many parts it carries, and how sloppily it is driven. See Race simulation for the AI driver itself and the parts reference for the part order.
raceStars in player/career/stars.ts gives 0 for a loss, otherwise 1 to 3. Each extra star is an independent test, so a clean run that only just wins still earns two:
| Star | Test |
|---|---|
| 1 | a win |
| +1 margin | won by at least MARGIN_SHARE of the opponent’s time, or the opponent never reached the line. In a bracket race this is replaced by running on your dial (onDial in bracket.ts) |
| +1 clean run | the launch plus all upshifts lost at most CLEAN_RUN_SHARE of the perfect run’s time, as graded by sim/runGrades.ts (the shift table on the results screen) |
The save keeps the best stars per event, never a lower result. Tournaments score by placing instead (see below). Bosses have no stars: the save records only whether they were beaten.
Prize money and progress
Section titled “Prize money and progress”player/career/progress.ts owns the career’s part of the save (CareerProgress: stars per event id, and bossesBeaten in order). Which districts are open is derived from bossesBeaten, never stored separately, so it cannot drift.
settleRace applies a finished race and returns a RaceAward:
| Outcome | Cash |
|---|---|
| First win of an event or boss | the full prize |
| Any later win | REPLAY_SHARE (a fifth) of the prize: the grind between first wins, so a first win still counts for five |
| Loss after crossing the line | CONSOLATION_SHARE of the prize, so a losing streak can still buy parts |
| Loss without reaching the line | nothing |
settleCareerRace.ts applies this to a garage; only the race server runs it. Wager bets are settled separately, see below.
The race server settles every career race (see Architecture): race/judgedStart.ts fetches a ticket with the AI’s seed before the race, and the results screen sends the input log and the tune raced; the server races the same race again (race/replayEventRace.ts, built by the shared race/eventRaceSetup.ts; it judges with judgeRaceResult from race/raceResult.ts, which leaves out the results screen’s charts and their extra replays, so a finish costs the server about 2 to 3 ms of CPU, measured 2026-11-11 on an M-series Mac) and pays what that earns (party/src/api/raceApi.ts). A wager’s ticket carries the bet, held to the limit (player/career/wagerRules.ts). A boss fight or tournament is a series: the server keeps its rounds (career_series, party/src/db/careerSeries.ts), each round’s ticket names its round, and the server builds that round’s distance or rival from the rounds before (player/career/seriesRules.ts) and settles the series at its end with the same rules as the game (bossFightRules.ts, tournamentRules.ts). The boss and tournament screens wait for that verdict (hub/roundVerdict.ts); without a connection the round does not count and the series is forfeited. Keep rounds won undoes the lost decider on the server for a confirmed ad.
Unlocks
Section titled “Unlocks”- Boss. Unlocks once the district’s stars reach
bossStarsNeeded:BOSS_UNLOCK_SHAREof the district’s maximum (events timesMAX_STARS), rounded up, both incareer.ts. Events inside an open district are never locked; the player can race them in any order. - Next district. Opens when the previous district’s boss is beaten for the first time (
isDistrictUnlockedinprogress.ts;settleRacereports it asunlockedDistrict). - Plain start.
nextRaceIdpicks what Enter on the main menu races: in the furthest open district, the first unwon event, else the waiting boss, else the first event short of full stars.
Boss fight
Section titled “Boss fight”A boss is a best of BOSS_ROUNDS.length, one round per distance in BOSS_ROUNDS order (eighth, quarter, half), first to BOSS_ROUNDS_TO_WIN takes it (career.ts). Before the fight, hub/scenes/BossIntro.ts lets the player pick each round’s tune slot of that round’s distance, starting from the lit ones (see Tuning).
player/career/bossFight.ts keeps the fight state (bossId, slots, round results) in the Phaser registry, not in the save. Leaving or reloading mid-fight forfeits it: the rounds won are lost, and racing him again costs 1 fuel. startBossRound is called by race/EventRace.ts as each boss race starts: it lights that round’s tune slot for its distance and returns the distance. After each round hub/scenes/BossRound.ts records the result, shows the score with a spoken boss line, and offers Next round or Forfeit.
When the player wins the fight, the race server settles it as a one-star race (settleBossFight in bossFightRules.ts). The first win goes to hub/scenes/PinkSlip.ts: the boss’s car joins the garage with his parts (Dex’s 6, Mara’s 12, Victor’s 11) and paint (bossCar in opponent.ts, skipped when that model is already owned), the prize is paid and the next district opens. A re-race win only pays the replay share, and the boss says a playerWinsAgain line (the car is already taken) instead of the playerWins line that hands it over (hub/scenes/BossRound.ts).
Leaving the pink slip (Career or Continue) asks the app store for its rating sheet (hub/storeReview.ts, through @capacitor-community/in-app-review: Play’s In-App Review API, Apple’s requestReview). A first boss win is the happiest moment the career has, and with four in Act 1 it is rare enough without a counter of our own: the store decides whether the sheet shows (Play has an undisclosed quota, iOS shows it at most 3 times in 365 days) and never tells the game, so nothing is saved. Asking on the way out keeps the sheet off the boss’s line and Raya’s beat. The apps only; the web builds have no store. On Android the sheet only appears in a build installed from Play (an internal test track will do), never in a sideloaded one.
player/career/series.ts treats a boss fight and a tournament alike: a race inside one has no restart, and quitting forfeits the whole series.
Event modes
Section titled “Event modes”EventMode in types.ts; modes.ts gives each its label on the event card.
| Mode | How it is won | Code |
|---|---|---|
headToHead |
first over the line | eventSpec.ts |
restricted |
head-to-head, but only cars passing the event’s rule may enter |
carRule.ts |
tournament |
three knockout rounds against seven entrants | tournament.ts, tournamentRun.ts |
bracket |
both dial in, the slower dial starts first, a breakout loses | bracket.ts, bracketDial.ts |
wager |
head-to-head with a bet on top of the prize | wager.ts |
roll |
both cars start rolling at ROLL_SPEED (sim/roll.ts), no launch |
sim/roll.ts |
Restricted
Section titled “Restricted”A CarRule combines optional carClass (exactly that class, set by the district), maxClass, maxHp, stock and noNitrous; every field set must hold. stock lets every car in, but races it with no part beyond the nitrous kit every car comes with and no part brands (carUnderRule, the weekly event’s stock week). noNitrous lets every car in, but underRule strips nitrous from both cars for the race (both applied in race/eventRaceSetup.ts). ruleLabel renders the rule for the card, for example “STREET CLASS ONLY” or “NO NITROUS”.
Car fact rules
Section titled “Car fact rules”Four more fields say what the car is, never what is fitted to it, so no part can dodge them
(player/career/carFacts.ts): layout (fwd, rwd or awd, from the car’s physics), engines (a list
of engine types; v8 and v8flat both read “V8”), era (model years from to to, both included, from
the car’s year) and origin (japanese, european or american, from the maker: carOrigin in
player/carName.ts). They read “FRONT-WHEEL DRIVE”, “V8 ONLY”, “80S AND 90S CARS”, “JAPANESE CARS”.
They are the rules of the districts after the Strip (docs/2026-11-10-endless-career-design.md,
“Cars: decided”): part rules (tyres, power, nitrous) stay for the weekly event. Which of today’s
cars each one lets in, by class, is on the cars reference.
Tournament
Section titled “Tournament”Eight seeds: the player is seed 0, the event’s entrants seeds 1 to 7. Round 1 pairs 0-1, 2-3, 4-5, 6-7; later rounds pair winners in order. A race between two AI entrants goes to the quicker average AI run (sim/aiRun.ts, cached), so the bracket is deterministic and the career check can play it exactly as the game does. Each round is raced with a tune slot of the event’s distance chosen on hub/scenes/Tournament.ts, and the garage cannot be visited in between.
A tournament is one event with stars by placing (tournamentStars): 0 out before the final, 1 for losing the final, 2 for winning, 3 for winning with every round’s run clean. Placing rather than a star per round keeps one event per tournament and stops round 1 alone from paying out. Cash in settleTournament (tournamentRules.ts) follows placing too: losing the final pays FINALIST_SHARE, winning the full prize; a better placing than before pays the difference, the same placing again a replay share, going out early the consolation.
Bracket
Section titled “Bracket”Each racer sets a dial-in, the time they expect to run. The slower dial gets the head start by the difference (opponentStartFrame, in sim frames), so two perfect runs to the dial finish together. wonBracket:
- A racer who did not finish never wins.
- Running quicker than your dial is a breakout and loses; if both break out, the smaller breakout wins.
- Otherwise whoever ran closer over their dial wins.
hub/scenes/BracketDial.ts suggests a dial of the analyzer’s perfect run plus DIAL_SUGGESTION_SHARE (a perfect-run dial would break out on any imperfect run). The AI dials its median seeded run plus AI_DIAL_SHARE; the median ignores its rare wheelspin disasters. The margin star becomes “on dial”: finishing within ON_DIAL_SHARE over it without breaking out. The player’s dial is kept in the registry for the event and its retries.
hub/scenes/Wager.ts lets the player bet in steps of BET_STEP, up to WAGER_SHARE of the opponent car’s value (model price plus its parts at their stage) and never more than the cash in hand (betLimit). A win adds the bet, a loss takes it; the race server applies it before the prize (applyWager in wagerRules.ts). The bet stays set for the event, so a retry races for the same stake.
How the hub starts races
Section titled “How the hub starts races”hub/scenes/Career.ts shows district tabs (locked until the previous boss falls), one column of event cards per crew member and the boss card on the right. It reopens on the district last shown, and jumps to a newly opened one. Four tabs fill the row; with more districts the row scrolls sideways (drag or mouse wheel, ui/scrollStrip.ts), the next tab peeks in at the edge, and the district on show is scrolled into the middle. Tapping a card goes through hub/startRace.ts:
tournament,bracketandwagerevents open their own screen first (Tournament,BracketDial,Wager), which then starts the race.- Other events start the
Racescene with the event id. If the current car fails a restricted event’s rule,hub/pickEligibleCar.tsopens a choice of owned cars that pass (it sets the picked one as current), or, when none does, offers the dealer. - The boss card starts
BossIntro, which runs the best of 3.
race/EventRace.ts turns the event id back into everything the race needs (the Phaser-free part is race/eventRaceSetup.ts): it builds the opponent (or the tournament round’s opponent), applies the rule, the dials and the roll speed, and afterwards routes the result to BossRound, Tournament, or Victory/Results.
Raya and story beats
Section titled “Raya and story beats”The guide, Raya Quinn, speaks short voiced and subtitled lines at fixed moments (“beats”). Her premise (docs/2026-11-06-story-guide-design.md): she lost her car, the Halden Fenrir that The Ghost drives as Strip boss, in a pink-slip race, and the four bosses are the road to winning it back. docs-site/src/content/docs/story/the-quinn-key.md is the full story and the source of her opening.
- On the first start the flashback comes first:
menu/scenes/FlashbackIntro.ts(beatflashback-intro), then the flashback race in Raya’s Halden Fenrir (player/story/flashback.ts: an easy rival, no prize, no race server), coached byrace/coach/flashbackCoach.ts. The coach plays theflashback-racelines throughui/guide/guideSubtitle.ts(a subtitle that does not stop the race) and slows the race throughRaceClock.timeScale: quarter speed on the lights, a hold at first gear’s shift point until the player shifts, slow motion until the nitrous fires. It also keeps the engine short of blowing, so the first race always finishes. The race skips Results formenu/scenes/FlashbackEnd.ts(beatflashback-end), which leads into the Prologue; its pause menu skips there too (race/raceHome.ts). - Then
menu/scenes/Prologue.tsplays thewelcomebeat (14 lines) over four story stills that change with her lines, then opens the main menu. The Epilogue (hub/scenes/Epilogue.ts) does the same with thefinalebeat after The Ghost’s pink slip, before the career hub.hub/scenes/DistrictIntro.tsdoes it for a district a pink slip opens: its arrive beat (harbor-arriveand so on) over the stills listed inhub/districtIntros.ts(assets/sprites/districts/), then the career hub. Playing the beat marks it seen, so the hub does not show it again; a district without stills (none today) would keep its arrive beat as a panel on the hub. All three are thin scenes overui/guide/storyStills.ts, which cross-fades a still per line through the guide panel’sonLineoption. The Prologue always plays (showBeat‘sreplayoption), so Settings’ “Watch the story” can replay it, starting from the flashback (FlashbackIntro, race included; its pause menu skips to the Prologue); the Intro is what limits it to the first start. player/story/beats.tslists everyBeatId.TIP_BEATSare the tutorial tips that Settings’ “Reset tips” shows again; story beats stay seen.player/story/careerBeats.tsandresultBeats.tsdecide which beats a screen may show: district arrivals, the first boss unlock, each boss’s before, lost and after lines, the races against Iris Quinn (Raya’s sister, in Harbor’s crew) and postgame. Results plays the first-win, first-loss and first three-star lines after career races only, so a quick race or a weekly event race cannot use them up.- Iris speaks in her own beats (
iris-before,iris-after): each line carries its speaker, and the panel swaps portrait and name per line (SPEAKERSinui/guide/guideVoice.ts). player/story/seen.tskeeps seen beats in the save (storySeen).ui/guide/showBeat.tsis the one call a scene makes; it marks a beat seen as it opens, so leaving halfway does not replay it.
Balance check
Section titled “Balance check”npm run career:check (scripts/career-check.ts) plays the whole career with a simulated player under a stated daily play pattern (scripts/playLength.ts) and reports the days each district takes. The targets hold for the player who watches no ads (NO_ADS), as most players never watch a rewarded one: three sessions a day far enough apart for the tank to refill, each spending a full tank, plus the day’s first-win gold spent on a tank, so 33 races a day and no daily bonus. For comparison the check prints, without pass or fail, the player who watches every ad that pays (EVERY_AD): the day’s three fuel ads, three gold ads (all the free gold, 25, spent on tanks for 8 more races) and three daily bonuses, so 44 races a day. Level-ups refill the tank too and interstitials pay nothing; the model counts neither.
The player races the career in order. Before a race it keeps its cars if one already wins. Otherwise it shops with all its cash (scripts/careerPlayer.ts): the part or stage with the most seconds saved per dollar, or a dealer car of the race’s class, then fits its brand tokens where they save the most. It only spends on cars that can win the race at all once fully built (maxedCar), so it saves up for the Halden Nocturne instead of sinking cash into the Halden Vesper pink slip, which The Ghost always beats. maxedCar sweeps every part to its quickest stage at most MAX_SWEEPS (4) times: the quick estimate is driven like the build before, so near the top two parts could otherwise trade places forever (they did once traction control made most top-stage parts worth only a few frames). If it still cannot win, it plays out the session replaying the best-paying race it has won, then tries again. Before a boss it replays the events where its cars now earn more stars until it has the boss’s stars. Prizes, replays, the daily bonus, tokens and the veteran bonus go through the game’s own rules (settleRace, applyDailyBonus, fitBrand, payVeteran). In a district with a rule the races past the open crew carry it (race.rule), so the shopper buys the rule car once no owned car can win, and grinds the open crew’s races until it can afford it. TEST_DISTRICT=1 npm run career:check adds the test district to the run. A race counts as won when a human at a share of the perfect run beats the opponent’s seeded AI runs. The check plays two drivers (scripts/careerTimes.ts): CASUAL at 93%, calibrated on 2026-11-13 to a self-described unskilled player who beat the Street boss in about an hour (about 28 races, the starter car, about 20 parts at stage 1 to 2, no tuning), never drives a clean run and earns at most 2 stars; GOOD at 98% earns the clean-run star too: a friend of Ruben’s drove like that on his first try. Cash beyond first wins comes from quick races (their real payout, QUICK_RACES_PER_DAY paid a day) or career replays, whichever pays more (scripts/careerCash.ts), and every career race costs the losses a driver averages before the win (the share of the rival’s seeded runs it beats). In a bracket race the player dials the analyzer’s suggestion, or its own time when that is quicker, as it would after breaking out once. To run in about a minute, the shopper ranks offers with a quick estimate (quickHumanTime: one race driven like the current build’s best run, simulateRunWith in sim/bestRun.ts) and as a lazy greedy (an offer’s last measured gain is kept and only the top offer is measured again until it stays on top); the race verdicts themselves use the full best-run search. It throws when the player is stuck for good, and exits 1 when:
- a district takes the casual no-ad driver outside its days, or the good one less than the minimum (
paceTarget): driving well may make the career quicker, never skip the grind. The targets: Street 4 to 6 (the hook), Harbor 20 to 29, Chinatown 24 to 34, the Strip 33 to 45 (ACT1_DAYS), every later district 35 to 49 (CHAPTER_DAYS: about 6 weeks, so the every-ad player, about 1.7 times as fast, finishes one about when the next comes out); or Act 1 outside 12 to 16 weeks (LAUNCH_DAYS), the same way. Tuned on 2026-11-12, against an 85% driver, by the district prizes alone (Street x1.1, Harbor x0.78, Chinatown x1.02, Strip x0.94 of the prizes before) to Street 4.8, Harbor 25.6, Chinatown 29.7 and Strip 39.3 days, Act 1 in 14.2 weeks; with every ad Street 3.0, Harbor 18.0, Chinatown 18.3, Strip 24.7 days, 9.1 weeks. Against the calibrated drivers that economy gives casual Street 0.8, Harbor 1.2, Chinatown 1.7, Strip 2.3 days and skilled Act 1 in 1.5 days: it fails until the economy is retuned; - driven by the good driver, as are the boss walls below: a stock car of the district’s cheapest class car wins an event of its last two crew members (each district must force an upgrade; a car of the district before cannot enter at all);
- a boss falls to a car he must stop (
scripts/careerBosses.ts): the previous boss’s car as won, every dealer car of the district before with every affordable part (a car of another class cannot enter, so it never takes the fight), and in Chinatown and on the Strip the previous boss’s car in its quickest build too, so those districts ask for a dealer car (STOPS_BUILT_PINK_SLIP). Elsewhere the previous boss’s car built up may win: it is a car of the district’s class, the ticket in; - a boss falls to none of his district’s dealer cars in their quickest build (
bossFallsTo); - the previous boss’s car, as won, wins more than
BOSS_CAR_SHAREof the next district’s events (scripts/careerFight.ts): the pink slip is a head start, not a skip.
The same run backs the assertions in tests/careerBalance.test.ts (about 30 s), which also checks that each district takes longer than the one before and that the starter car wins the first event without buying anything. Run the check after changing any district file, opponent, price, part or sim constant. The numbers it reached are in docs/2026-11-08-progression-design.md, “Economy as built”.
Pace with and without ads
Section titled “Pace with and without ads”Rewarded ads pay time: fuel (more races a day), gold (spent on full tanks) and the daily cash bonus. The pace targets above are measured on the player who watches none of them (33 races a day, no bonus); one who watches every one races 44 times a day with the bonus and is about 1.7 times as fast (Act 1 in about 9 weeks against 14). Interstitials between races pay nothing and are not counted.
What the research says (docs/2026-11-12-rewarded-ads-pacing-research.md, 2026-11-12):
- No published source gives a “right” ratio between an ad watcher’s and a non-watcher’s pace, so 1.7 is neither confirmed nor ruled out; a follow-up search found none either. The sources agree on the method instead: model several players side by side, cap the ads, tune the cap on real data.
- The one pace figure found (AdReact, a vendor blog, no derivation): ads should let a patient player progress at 60 to 70% of a paying player’s pace.
- Daily caps on rewarded views quoted for mid-core games: 10 to 15 (same blog). Ours is 9.
- An unsourced Gemini overview puts ad refills at 25 to 50% more sessions and ad multipliers at 1.5 to
2 times the wealth in games without timers; ours are +33% races and about 1.7 times the pace, so not an
outlier on paper. It also names the usual way to sell the gap, a pass that gives every ad bonus
without the video: the planned ad-free pass (
docs/TODO.md). Nitro Legend has no interstitials since 2026-11-08, so a player who watches no rewarded ad sees no ads at all. - Only about 25 to 40% of daily players watch rewarded ads at all (Unity 2024, an ad vendor’s figure): most players get the no-ad pace, and the every-ad player is a ceiling, not the average.
- Daily caps of about 3 per placement are common practice, which matches ours.
- The risk of a large gap is lost gold sales (ads give what gold sells), not non-watchers leaving: Rovio’s test showed rewarded ads offered to everyone lowered purchases; doubling ad exposure barely changed retention.
Decided (2026-11-12): the targets moved to the no-ad player, Act 1 in about 14 weeks, with Street sped up to about 5 days so the first boss still comes fast. The gap stays at about 1.7 until real players show what it should be: beta testers and a soft launch measure the share of players who watch rewarded ads, ads per player a day, days to each boss with and without ads, and retention.