Big Typing Quiz Game Bible Play

Big Typing Quiz: the Game Bible

The complete design and reference document for Big Typing Quiz. It is two things at once, on purpose: the specification a developer works from, and the manual a player reads when they want to know what a Railgun actually does. Every number in it is the number the code uses.

About this document#

Who it is for. A developer changing a system, a designer arguing with a balance decision, and a player who has just been told that splash rolls off plating and wants to know by how much. Those three readers want the same facts at different depths, so each system is described in the same order: what it is for, how it behaves, then the exact parameters in a table.

Where the truth lives. The code is the truth, this document describes it. Every table here was read out of the modules under js/, and the derived figures (XP per minute, level costs, characters to kill a word) were computed by running those modules rather than by hand. When the two disagree the code wins and this file is stale: fix it in the same commit that moved the number.

How to read a parameter table. Anything named as a multiplier is applied to a base value rather than replacing it. Anything in milliseconds is a real clock. Positions and speeds in the arcade are FRACTIONS of the playfield and playfield heights per second, never pixels, because what matters is how long a threat takes to arrive and not how big the screen is.

A note on units. One word is five characters, everywhere, without exception (CHARS_PER_WORD in js/util.js). Words per minute means correct characters divided by five, over the minutes actually spent typing. Every WPM in this document is net WPM unless it says otherwise.

Conventions. File paths point at the module that owns a rule. A number in code style is a constant you can find by that name. Sections are anchored, so any heading in this document can be linked to directly.

The other documents. ART_DIRECTION.md covers the art: how it was made, what each family has to look like, and the compositing contracts this document only summarises. CLAUDE.md is the guide for AI editors and carries the same reasoning in a different order. FILE_MANIFEST.md describes every file in the repo. FIREBASE_SETUP.md has the Firestore rules. This file is the one that carries the numbers.

1. The brief#

A complete beginner and a 130 words per minute typist have to open the same page and both find it worth playing.

That sentence is the whole design problem, and almost every decision in this document is an answer to it. It is harder than it sounds, because the two audiences want opposite things:

Sites that pick one audience are common and they are all the same site. Sites that try to serve both usually do it with a difficulty menu, which fails because a difficulty menu asks a beginner to self-assess before they have any idea what the scale is.

Big Typing Quiz answers it four ways at once, and the four are deliberately different shapes so that no single player can be at the bottom of all of them:

AxisWhat it measuresShapeWho it favours
Level XPAbsolute typing speedSuperlinear (a 1.35 power of pace)Fast typists, heavily
MasteryImprovement against your own pastRatio, relative to your own baselineEverybody equally
ChipsTime spent and consistencyLinear in minutes, not speed-scaledSlow, regular players
GearAccess to harder contentReduces failure, barely touches earningAnyone who plays

And two mechanisms remove the need to self-assess at all:

1.1 The rules the design will not break#

These are load bearing. Everything else is negotiable.

  1. Nothing is ever gated. Every game, drill, curriculum node, Academy group, sector and vendor is reachable in the first minute. Progression is shown as a score, a star count or a badge, never as a lock. A sector states a recommended power and lets you in regardless.
  2. Gear buys access, skill buys rate. Gear reduces failure so you survive deeper. It never multiplies XP.
  3. Assistance costs score. Every item that types for you carries a score penalty, so running with no help at all is the highest scoring build in the game.
  4. Accuracy is paid in time. A threat's health is its word length, so being accurate literally means typing fewer characters. Nothing is ever deducted for a mistake in the arcade: you spend time instead.
  5. Only complete runs count. A run walked out of is discarded everywhere except the per-key model, which keeps the keystrokes because they are still real evidence about your fingers.
  6. One funnel. finishRun() in js/run.js is the only thing in the codebase that grants XP, chips, cores, badges, loot, bests, History entries or leaderboard posts. A game reports what happened; the funnel decides what it was worth.

2. How to play#

This section is the manual. Everything in it is expanded later.

2.1 The first ten minutes#

  1. The Flight Check offers itself once. One minute of ordinary words. It measures you, grants the level you have already earned, seeds the per-key model with real evidence, and tells the Academy which key groups you have already proved. Declining costs nothing and it never nags again, but the offer stands on the Arcade card and in Progress until you take it.
  2. Open the Arcade and play Starfall Keys. Craft descend carrying words. Type a word and the turret fires on whatever is carrying it. Miss and the heat builds. Every fifth wave a boss broadcasts a paragraph at you.
  3. Spend chips in the Armory. Chips accrue from time played, so you will have some. Buy a hull or a coolant first: staying alive longer is what makes everything else pay.
  4. If the keyboard is not yet under your fingers, open Training. The Academy teaches two keys at a time and never scores you on speed.

2.2 Controls#

WhereKeyWhat it does
Arcadeany printable characterLocks the lowest threat whose word starts with it, then types into that lock
ArcadeTabSwaps to the other weapon hardpoint (only if one is fitted)
ArcadeCtrl + SpaceSpends an emergency charge on whatever is closest to your base
ArcadeEscapeDrops the current lock, at no cost
Arcadeany key during a wave cardSkips the breather and starts the next wave
Boss transmissionBackspaceCorrects a character (desktop only)
Sprint and drillsBackspaceCorrects a character. It restores the text but never refunds the accuracy
Anywherethe tab barSix sections: Arcade, Training, Versus, Progress, Leaderboards, Armory

On a phone the keyboard is raised by a single hidden input (#kbCatch) that the app never rebuilds, so it stays up for the whole run.

2.3 Reading the arcade HUD#

ReadoutMeaning
HullBase health. At zero the run ends and is scored
HeatRises on every mistype, bleeds off over time. Full means the turret jams for just over a second
ComboConsecutive correct characters. Drives the score multiplier and the sound
WaveThe wave counter. Every fifth is a boss (every fourth in the Rift)
ScoreWhat the leaderboards rank. Multiplied by combo, the Zone, FOCUS, your loadout and the difficulty
ChargesEmergency charges left, if your module carries any
GunsBoth hardpoints, with the live one lit

2.4 Ten things worth knowing early#

  1. Typing the first letter of a word locks the LOWEST threat carrying it. The lowest one is the one about to land.
  2. A wrong key never deducts anything. It costs heat, the combo, and (in Strict mode) the lock.
  3. Power-ups are typed like anything else. They are the short words in capitals.
  4. A boss is not a wave with a health bar. It broadcasts prose at you, and while it does, your typing is the only weapon that fires at the escorts.
  5. Being accurate with a precision weapon kills a word before you finish typing it. That saved time is the reward.
  6. Fumbling with a precision weapon means the target survives and takes a fresh short word. Nothing is deducted, you just meet it twice.
  7. Gambits are opt-in handicaps that pay a multiplier. They are how you make the game hard again after your gear has made it easy.
  8. Cores only ever drop from bosses and mini-bosses. Nothing else in the game produces one.
  9. The Academy never measures speed. Certifying the whole keyboard is the largest single XP award in the game.
  10. Contracts, the streak and the Salvager's shelf all roll over at your local midnight.

3. The loops#

A game that only has one reward cycle only holds one kind of player. Five run at once, and each is a different length:

LoopLengthWhat closes it
The keystrokeUnder a secondA correct character: a rising pitch, a beam, damage
The waveAbout a minuteThreats cleared, a wave card, a score milestone, an event banner
The encounterMinutesA boss killed and salvage falling out of it
The sessionTwenty minutes to an hourLevels, certifications, badges, a vendor's stock
The long gameWeeksThe collection, the diploma, the boards, ascension

The middle loop is the one most typing sites do not have at all, and it is why the loot system exists: XP is continuous and predictable, chips accrue by the second, and a badge fires once. None of those produce the moment where something falls out of a boss and you do not yet know what it is.

4. Progression#

4.1 Axis 1: level XP, absolute skill#

XP counts correct characters produced under pressure, which is what typing skill is. The pace half of a run's XP is:

words   = correct characters / 5
paceXp  = words * paceFactor(netWpm) * accFactor(acc) * difficultyMultiplier

with

paceFactor(w) = clamp((w / 40) ^ 1.35, 0.35, 8)
accFactor(a)  = clamp((a / 100) ^ 2, 0, 1)

Accuracy is squared, so speed bought with sloppiness does not pay: 95% keeps 90% of the XP, 85% keeps 72%, 70% keeps 49%.

The reference pace is 40 WPM, where paceFactor is exactly 1. Because a faster typist also produces more words in the same minute, the compounded rate is steeper than the exponent alone suggests:

Net WPMpaceFactorXP per minute at 100%at 95%
150.350 (floor)55
200.39287
250.5301312
300.6782018
401.0004036
501.3526861
601.72910494
702.129149134
802.549204184
902.988269243
1003.445345311
1103.918431389
1204.407529477
1304.910638576
1405.426760686
1606.4981040938

Both clamps matter. The floor of 0.35 keeps a genuine beginner earning something rather than nothing, which would read as broken. The ceiling of 8 stops a freak measurement (a stuck key, a paste, a three character run with a tiny denominator) from minting a level.

Flat bonuses are the beginner's engine. They are fixed amounts, so they dominate the XP of somebody hitting firsts constantly and are a rounding error to an expert:

BonusXPWhen
firstRun250The first completed run of a game
personalBest150A new best for this game and mode
dailyFirst200The first completed run of the day, any game
perfectRun220100% accuracy on a run of at least 60 characters
waveMilestone60Per multiple of 5 waves reached in the arcade
nodeFirstClear200First time a curriculum node is cleared
nodeStar120Each new star on a curriculum node
academyCertified300Each Academy key group certified
academyGraduated3000Certifying all 21 groups. The largest flat award in the game

Defined in BONUS, js/xp.js.

The level curve costs round(60 + 40 * level^1.28) XP for the step from level to level + 1, capped at level 100. The exponent is gentle enough early that a beginner sees levels move in one session, and steep enough later that level 60 means something.

LevelCost of the next levelTotal XP to reach
11000
2157100
5374776
108223,508
201,91116,457
303,17041,115
424,84488,209
506,041131,112
607,613198,527
8010,975382,270
100(capped)635,670

Ranks follow the level, so the number and the word never disagree:

LevelRank
1Cadet
5Recruit
10Ensign
15Pilot
20Lieutenant
30Commander
40Captain
50Ace
65Wing Commander
80Vanguard
95Legend
100Grandmaster

4.2 Axis 2: mastery, relative to yourself#

Curriculum nodes and Academy drills are judged against your OWN rolling baseline, never against a fixed target. That is the mechanism that keeps a beginner progressing, and it is the thing most star-based typing sites get wrong: fixed WPM targets give the beginner who needs encouragement most none at all, and let the expert clear everything on day one.

How a node is starred (scoreNode in js/skill.js):

RuleValue
Accuracy needed to clear the node at all92%
First cleared runEarns one star by definition, and becomes the baseline
Further starsLand at 1.05x, 1.12x and 1.22x of your own baseline for that node
Third star also requires96% accuracy
Stars are never taken awayOnce awarded, permanent
Baseline rise on a better run35% of the gap, immediately
Baseline fall on a worse cleared run6% of the gap
Minimum baseline8 WPM, so nothing divides by nearly zero

Because the steps are RATIOS, 18 to 24 WPM (plus 33%) and 100 to 115 WPM (plus 15%) are the same kind of event, and the beginner's is in fact the larger one. The baseline falls at roughly a sixth of the rate it rises, so it is a floor you have to walk back down to rather than a punishment for one good night.

4.3 Axis 3: chips, time and consistency#

Chips are the Armory currency and they are deliberately NOT speed-scaled:

chips = correctCharacters / 5 * 0.5 + minutes * 8
        + 10 if accuracy >= 97
        + 25 if the run was a personal best

then multiplied by the salvage multiplier from gear and the sector, and by any ascension bonus.

This is the counterweight to XP's speed bias, and the numbers show it working:

SessionChipsXP
5 minutes at 25 WPM10361
5 minutes at 120 WPM3402,437
20 minutes at 25 WPM410244
20 minutes at 60 WPM7601,912
20 minutes at 120 WPM1,3609,747
60 minutes at 25 WPM1,230733

A slow typist who plays for twenty minutes out-earns a fast one who plays for five (410 chips against 340), while the fast one has earned forty times the XP. Both are progressing, on different axes, and neither route is a trap.

4.4 Axis 4: gear, access rather than rate#

Stated as one line, because it is the thing to protect if you change anything in js/gear.js:

Gear buys ACCESS. Skill buys RATE.

Almost every stat in the catalogue reduces FAILURE: more health, less damage taken, more heat before a jam, a combo that survives a slip, a charge that swats the thing about to land, a field that descends slower. Good gear lets a 30 WPM player survive to wave 18 in a sector that would have ended them at wave 6, and carries them into the Rift, which is exactly what it is for.

The only two stats that touch earning are scoreMult and salvageMult, and both are clamped hard: score to 0.75x..1.45x, salvage to 0.8x..1.6x. Compare that to paceFactor, which spans 0.35x to 8x on typing speed alone. A fully kitted 30 WPM player reaches the Rift and earns steadily; a fast player in starting gear out-earns them several times over while dying earlier.

The full gear system is section 10.

4.5 The placement#

The Flight Check is the skip-ahead. It runs a 60 second sprint and then grants a lump sum equal to the level that performance deserves.

placementLevel(w, acc) = clamp(round(((w - 18) / 3.6) * (acc / 100) ^ 1.5), 1, 42)
                         (and exactly 1 below 20 WPM)
placementGrant(...)    = max(0, totalXpFor(placementLevel) - currentTotalXp)
Net WPMLevel at 100%at 97%at 92%XP granted to a new account at 97%
201110
30333257
406651,150
509882,149
601211104,330
701414137,400
801716159,973
9020191814,664
10023222020,398
11026242324,822
12028272532,392
13031302741,115
16039383570,275
180+42 (cap)424088,209

Four rules make it safe to re-take:

  1. It only ever raises. The grant is the difference between what you have and what the measurement is worth, floored at zero.
  2. The profile keeps the BEST placement, not the latest. Rewriting "placed at level 22 from 96 WPM" with a slower re-take would replace a true sentence with a misleading one.
  3. It seeds the per-key model, and that seeding also only ever raises. This is what makes the curriculum immediately recommend symbols and code to a proficient typist rather than the home row.
  4. It hands the Academy a head start. Any key group whose keys the measurement already proved (12 or more samples at 90% or better) is marked as evidence rather than starting from zero.

The dialog offers itself once per profile and then never again. placed records a completed check; placeOffered is stamped the moment the dialog opens, because closing it is an answer and re-opening a modal on every load until a one minute test is finished is nagging. The offer itself does not go away: the Arcade's "Start here" card and the Progress re-take button stand for as long as the profile is unplaced. tools/check-app.py guards this.

4.6 Per-game ranks#

Each mini-game keeps its own XP, rank, plays, bests and badges in state.games[<id>], so playing Starfall levels up Starfall AND the account, and being level 40 does not make a game you have never opened look finished.

The per-game curve is flatter than the account one, at round(120 + 70 * rank^1.15) per rank, capped at rank 50. A game rank is a familiarity measure, not a skill measure.

Game rankCost of the nextCumulative
11900
55661,298
101,1095,191
202,31421,602
50(capped)149,110

4.7 Ascension#

At level 100 you may reset the level and XP, keeping EVERYTHING else: gear, cosmetics, badges, the Academy, History, the boards. Each ascension grants a permanent +6% to XP and salvage (compounding by count), a visible mark, and 5 cores so the first hour back is not a wasteland.

It is opt-in and never suggested twice. It exists so the ceiling is a door rather than a wall.

5. Measurement#

5.1 Speed and accuracy#

There is exactly one definition of each, in js/util.js, and every surface in the app uses it:

CHARS_PER_WORD = 5
wpm      = (characters / 5) / minutes
netWpm   = (CORRECT characters / 5) / minutes
accuracy = correct / typed * 100

What counts as a minute. The arcade measures typing time as wall clock minus the time the game spent showing you a card rather than a wave (the countdown and the between-wave breather). The wall clock still counts for chips and History, because that IS time played, but calling a four second breather zero WPM and averaging it into a leaderboard row would be dishonest.

Backspace. In the arcade a mistyped key is spent and there is nothing to delete. In sprints, drills and boss transmissions backspace is allowed, because a paragraph you cannot correct is a punishment rather than a test. It restores the text and the character counts toward net WPM, but the keystroke that got it wrong stays in the accuracy denominator forever. That is the standard convention and the only one that stops "type garbage and repair it" scoring 100%.

5.2 The plausibility gate#

plausible() in js/run.js drops a run that claims something no human did. The Cloudflare Worker holds the same line independently on the leaderboard side.

RuleValue
Must be completecomplete: false is discarded everywhere
Minimum duration2,000 ms
Must have typed somethingtyped > 0
Correct cannot exceed typedenforced
Maximum net WPM260

5.3 The per-key model#

One record per character and per typed pair, holding how often it was hit, how often it was right, and how long the finger took. This is what makes the site personal: the arcade builds asteroid chunks from your worst keys, the drills target them by name, and the Progress heat map is a picture of your own hands.

ParameterValueWhy
MIN_KEY_MS40 msFaster than this is a paste or a stuck key, not a finger
MAX_KEY_MS3,000 msSlower is somebody answering the door
KEY_INERTIA0.25Old evidence is discounted, so the model notices improvement
KEY_ALPHA_MIN0.1A veteran's record stays movable
KEY_CAP140 charactersThe blob syncs and has to stay small
BIGRAM_CAP400 pairsSame
MIN_KEY_SAMPLE12 pressesBefore a key may be called weak
ACC_FLOOR70%As bad as the accuracy scale goes for one key
LAT_SLOW / LAT_FAST1.8x / 0.6xOf your own average pace, scoring 0 and 100
DEFAULT_REF_MS320 msStand-in pace before you have been measured

Timing is keyed by the character that was EXPECTED, not the one that was pressed: knowing you typed k when l was wanted says something about l.

Account-level skill is an exponential moving average of recent runs rather than a best, because a best is a story about one lucky minute and this number has to decide what to put in front of you next. The first run is adopted whole (a new player should see a real number, not a fifth of one), then the weight falls off as runs accumulate and settles at a floor of 0.12 for a veteran. Run length weighs in as well: a 15 second burst is not the same evidence as a five minute haul (RUN_FULL_MS is 30 seconds).

5.4 Word tiers#

The lexicon is ten tiers of hand-written real words, each staying strictly inside its own character set, plus a prose tier for sentences and paragraphs.

TierNameCharactersWordsChosen at
homeHome rowasdfghjkl;69under 18 WPM
homeTopHome and top rows+ qwertyuiop41118 to 27
lettersEvery lettera to z33228 to 37
commonCommon wordsa to z68138 to 51
commonLongLonger wordsa to z76752 to 67
capitalsCapitals+ A to Z20168 to 84
punctPunctuation+ ,.;:!?()-'"16685 to 99
numbersNumbers+ digits and .:/-,$%127(by node)
symbolsSymbolsletters, digits, @#$%^&*_+=/<>~-! plus the vertical bar and the backtick118(by node)
codeCodeeverything183100+
proseProsesentences with punctuation371(by node)

Plus 20 quotes for the sprint's Quote mode.

tierForWpm in js/xp.js maps the rolling skill number onto that ladder, and it is the single reason the same arcade game is right for both audiences. Difficulty and sector multiply on top; they never replace it. A fixed table would mean picking an audience.

5.5 Weak keys#

weakKeys(n) returns the worst keys the model is confident about (at least 12 presses), scored on accuracy first and latency second. They are used for:

Your weak keys are the one thing a Versus room does NOT share.

6. Modes and surfaces#

The app is six tabs. Two of them hold games, the rest are where the game is read, spent and compared.

TabIdWhat is there
ArcadearcadeThe game hub (one card per registry entry) staged as an operations deck with a holographic sector chart, the pre-flight briefing, and the running game. The hub and the game are never on screen together
TrainingtrainThe Academy on top, the curriculum (Flight School) below
VersusversusHead to head racing, signed in with a claimed name
ProgressprogressWhat is open today, how good you are now, what you have done
LeaderboardsranksEight opt-in boards
ArmoryarmoryStaged as a hangar bay: the loadout first (it is the point of the economy), then the three vendors, then cosmetics and the collection

The router mirrors the active tab into location.hash, so app.html#arcade deep-links. The ids are stable even if a caption is renamed.

6.1 The game catalogue#

js/games/registry.js is the only list of what games exist.

GameIdStatusModesBoard scope
Starfall KeysstarfallLiveOne mode (Endless), four difficultiesDifficulty, plus a deepest-wave board
Flight ChecksprintLive60 seconds, 15 seconds, Quote, Weak keysThe two timed modes
Wing CommandcoopAnnouncedPair, Squadron(none yet)
The ForgeforgeAnnouncedWorst five, Transitions, The ladder(none yet)
TerminalterminalAnnouncedTokens, Lines, Symbol storm(none yet)

An announced card is not a lock. It is a game that has not been BUILT yet, it says so in those words, and it must never show a padlock, a level or any implication that the player fell short. Nothing on this site is gated; the only thing standing between a player and an announced game is us.

7. Starfall Keys#

The flagship. Your base sits on the lower edge, craft descend carrying words, typing a word destroys the thing carrying it.

7.1 The field#

Simulation units are FRACTIONS of the playfield, so nothing depends on canvas size and a phone turned over mid-run never moves a threat relative to the base.

ConstantValueMeaning
STEP1/60 sFixed timestep, with an accumulator, so a 144Hz laptop and a throttled phone run the same simulation
MAX_FRAME0.25 sThe most a slept tab may catch up in one frame
VIEW_TOP-0.04The sliver above the field a craft drifts in from
REVEAL_TOP0.12The deepest strip the reveal stat can buy
BASE_Y0.93A threat at or past this has arrived
MAX_PARTICLES260Decoration ceiling

Descent speed is in playfield heights per second, which is the honest unit: what matters is how long a threat takes to arrive.

7.2 The wave cycle#

countdown (2200 ms, skippable after 700 ms)
  -> wave: spawn queue drains, threats descend, encounters fire
  -> wave ends when the queue is empty and nothing but power-ups is flying
  -> breather (4200 ms, skippable by any key), hull regen applies
  -> next wave

What a wave is built from (waveBase, fillQueue):

pace     = clamp(currentWpm() / 40, 0.55, 2.4)
speed    = 0.026 * (1 + (wave - 1) * 0.055) * pace * difficulty.speed * sector.speed * fieldSlow
interval = clamp(2900 / (1 + (wave - 1) * 0.17) / pace, 520, 3400) ms
wordLen  = 1 + (wave - 1) * 0.03
queue    = min(46, round(6 + wave * 1.7)) threats

Each spawn then jitters its own gap by interval * (0.75 + rnd * 0.5).

The adaptive part is the pace term. It is the measured player, so the field moves at the speed of the hands playing it. Difficulty and sector multiply on top rather than replacing it: a beginner on Nightmare is still reading short words, they are simply arriving quickly.

Word tier per wave climbs off the MEASURED tier, not the wave number:

waveTier(wave) = TIER_ORDER[ clamp(indexOf(tierNow()) + floor((wave - 1) / 4), 0, last) ]

So an expert who starts on punctuation is reading code by wave twenty, and a beginner who starts on the home row has reached whole words.

7.3 Enemy catalogue#

A threat's HEALTH IS ITS WORD LENGTH. With the starter weapon that is one point of damage per correct character, so finishing the word kills it and the game reads exactly as it would with no gear system at all. That baseline is deliberate and must stay true.

ThreatRadiusDamage on arrivalWord lengthKill scoreSpawn weightFrom waveSpecial behaviour
Drone1563 to 50101The baseline threat. Nothing special
Cruiser27138 to 124033Slow and long. Drops a power-up 85% of the time
Asteroid2193 to 62542Its "word" is a chunk built from YOUR weakest keys
Splitter1975 to 72034On death by typing or splash it becomes two drones at 1.35x speed
Power-up130the capital wordnone(dropped)anyFalls at 0.55x speed. Missing one costs nothing

Word length ranges are multiplied by the wave's wordLen and by any live event's length multiplier, then floored at 2.

Threat behaviour is deliberately simple, and that is a design decision rather than an omission: everything interesting in this game happens in the text, so a threat that jinked or shot back would compete with the words for the player's eyes. What a threat does is descend at its speed, drift sideways by up to +/-0.006 per second, and land.

A splitter's children do not split, and neither splits when killed by a blast (a nuke or an emergency charge), because a panic button that seeded two fresh drones would be worse than useless against exactly the wave it exists for.

Arrival. A threat crossing BASE_Y deals its damage, scaled by the sector's toughness and your hull's damage multiplier, absorbed by any shield first. A power-up that arrives simply leaves.

7.4 Targeting and the lock#

RuleBehaviour
AcquiringThe first character locks the LOWEST threat whose word starts with it and whose typing has not begun. Lowest is both the most urgent and the one your eye is already on
While lockedEvery later keystroke belongs to that lock. There is no target switching until the lock ends
Committing revealsA hidden word (Blackout, the Oracle) shows itself the moment you lock it
A wrong keyCosts heat and the combo, and marks the target as fumbled for the rest of this engagement
AssistsSpent BEFORE the lock is. An assist forgives one wrong character, works in Strict mode too, and refills at the start of each wave
Strict modeDrops the lock on a wrong key once assists are gone
Forgiving modeKeeps the lock. The mistake still costs heat, accuracy and the combo
EscapeDrops the lock deliberately, at no cost, so you are never stuck typing three wrong characters to shake off a target
The word running outIf the target survives its word it takes a FRESH SHORT word (3 to 5 characters, sized from remaining health) and starts clean

Strict mode default follows the difficulty (auto): strict from Ace upward, which is where the difficulty blurbs already promise it. The player can override to on or off in settings.

7.5 Damage and the accuracy axis#

Every correct character deals damageFor() to the locked target:

d = clean ? dmgClean : dmgFoul
d = d * profile.clean or profile.foul     (the encounter's vulnerability, if any)
d = d * (1 + min(0.6, comboDmg * combo))  (comboDmg weapons only)
d = d * pierce * profile.pierce           (against a boss or mini-boss only)
d = d * swapDamageMult(msSinceSwap)       (0.5 rising to 1 over 600 ms)
floor at 0.05

clean is whether the CURRENT TARGET has been fumbled, not whether the run has. The bargain is per engagement: a slip costs you that kill and nothing else, which keeps it a cost rather than a spiral.

What this feels like. With a Lance (1.55 clean, 0.45 foul) a clean seven-letter word dies on the fifth character and the last two simply vanish. Fumble the same word and it takes sixteen characters. Nothing is deducted and nothing scolds you: you spend the one thing you cannot get back.

Splash deals splash * targetMaxHp to the nearest other threat within a squared distance of 0.35 (with x scaled by 1.4 for aspect), and is drawn as an arc so it never reads as the game killing things at random. Splash cannot chain-trigger power-ups and does not reach the encounter's own craft.

7.6 Heat and jams#

ConstantValue
Heat per mistype13, times any event heat multiplier
Heat capacity100 base, heatCap from gear (clamped 50 to 320)
Heat decay20 per second base, heatDecay from gear (clamped 8 to 80)
Jam duration1,150 ms
Heat left after a jam45% of capacity
Wave startHeat resets to 0, or to 50% under the Hot Start gambit

A jam drops the lock, shakes the screen and takes NO input at all while it lasts. It deliberately does not count the ignored keys as mistakes: the mistake that caused the jam has already been paid for, and charging again for the second you cannot fire would make heat a spiral rather than a lesson.

7.7 Combo, score and the Zone#

Combo is consecutive correct characters. A mistype takes it to floor(combo * comboKeep), where comboKeep is 0 without gear (lose it all) and up to 0.85 with it.

Score is small numbers multiplied by everything:

EventBase points
A correct character2
A kill12 + (word length x 6) + the kind's own score
A word killed with no slip at all30, times any event perfect multiplier
A boss phase stripped400
A mini-boss destroyed600
A boss destroyed1,500
A wave cleared100 x wave number
scoreMultiplier = comboStep * zone * focus * sectorAndLoadout * difficulty
  comboStep = min(4, 1 + floor(combo / 10) * 0.25)
  zone      = 1.25 while lit
  focus     = 2 while a FOCUS power-up is live

The Zone is the flow channel, named and paid for:

ParameterValue
Combo needed to light it20
Recent accuracy needed96%, over a window of the last 40 keystrokes (at least 12)
Misses tolerated before it drops1
Score while litx1.25
XP per second held3.2, scaled by your pace factor

It pays PER SECOND HELD rather than per entry, so it rewards sustaining flow rather than tapping into it. A boss transmission is its natural home, because it is the longest unbroken stretch of typing the game offers. The visual treatment is deliberately calm: the Zone is the player concentrating, and the worst thing the game could do at that moment is interrupt them to celebrate.

7.8 Power-ups and charges#

Power-ups are ordinary lockable threats that happen to do something good when they die, which is why they are typed exactly like everything else. They are uppercase so they read as a command rather than as another word to clear.

WordEffectDuration
NUKEDestroys every threat on screen except bosses and other power-upsinstant
SHIELDRestores 22% of maximum hull and adds 20 shieldinstant
SLOWThe whole field descends at half speed8,000 ms
FOCUSDouble score10,000 ms
EMPSpawning is frozen6,000 ms

Drop rules. Exactly one roll per kill: 85% from a cruiser (which is what power-ups are for, since a cruiser is slow, long and worth engaging), 3% from anything else, and never from a power-up itself.

Emergency charges (Ctrl + Space) destroy whatever is closest to your base, skipping power-ups, because spending a panic button on the thing you were about to collect would be the worst possible outcome of pressing it. Charges come from the module slot and are capped at 8.

7.9 Difficulties and sectors#

Difficulty scales speed and pays a multiplier on XP and score:

DifficultyXP and score multiplierDescent speedStrict by default
Cadet0.8x0.75xNo
Pilot1.0x1.0xNo
Ace1.3x1.3xYes
Nightmare1.7x1.7xYes

Sectors are where gear turns into access. Every one is enterable at any power: the recommendation is advice, not a lock.

SectorRecommended powerSpeedDamage takenSalvageCoresBoss every
Home Sector01.00x1.00x1.00x1.0x5 waves
The Belt261.14x1.10x1.25x1.2x5 waves
Deep Field581.32x1.25x1.60x1.5x5 waves
The Rift921.55x1.45x2.20x2.2x4 waves

The sector's damage column multiplies what reaches your hull AND the health of a boss phase (a phase's health is the length of the transmission that opens it, times this number).

Skill substitutes for gear, at SKILL_POWER_PER_WPM = 1.2 points of power per WPM above the 40 WPM reference. So roughly every 10 WPM is worth 12 points of power, because a threat destroyed early never tests your hull at all. sectorAdvice() says this in words rather than blocking anybody.

7.10 The end of a run#

A run ends when the hull reaches zero, or when an optional duration expires (a timed Versus round, which counts as complete because the player played the whole thing). Leaving the tab or navigating away calls stop(), which abandons: no score, no XP, no History, no board, and only the keystrokes go to the skill model.

What Starfall reports to finishRun():

FieldContents
game, modestarfall, and the board scope (the difficulty, or versus)
completeWhether the run ended properly
durationMs, typed, correct, wrong, wpm, netWpm, accThe plain measurements
keysThe per-character and per-bigram tally, with timings
score, wave, combo, difficultyThe arcade extras
meta.eventsThe encounter ledger: what fired, whether it cleared, its multipliers, characters typed under it
meta.zoneMsMilliseconds held in the Zone
meta.sector, meta.lootMult, meta.gambitMultWhat was flown and what was bet
meta.medianMsMedian keystroke latency, for the worker

The game computes NO rewards. It reports facts, and js/run.js decides what they were worth.

7.11 The two renderers#

Starfall has two views and they are interchangeable: the flat one (a hand-drawn 2D canvas with the sprite layer of section 7.12 over it) and a 3D scene built on the vendored Three.js. Which one is running is a question about the picture and never about the game.

The flat view is the site. It is what every ordinary visit gets. The 3D renderer is still being worked through, so it does not ride along with the home page: it has its own address.

URLWhat runs
bigtypingquiz.comFlat, the shipped game
bigtypingquiz.com/3d3D, in development. Redirects to app.html?3d=1#arcade
bigtypingquiz.com/2dFlat again. Redirects to app.html?3d=0#arcade

Following the link sets settings.render3d and PERSISTS it, so the URL is an entry point rather than something to keep in the address bar, and the Settings switch does the same thing and is the way back for anybody who arrived by link. The redirects live in _redirects, a Cloudflare Pages file that a plain local serve does not reproduce; the query parameter it lands on is what actually does the work.

The rules that keep the two views one game:

RuleWhy
A view READS the run state and never writes to itIt is what makes them interchangeable rather than two forks of a game
Neither may draw from the run's seeded generatorA renderer that consumed simulation randomness would desynchronise a Versus race by being pretty. The 3D view seeds its own generators off R.seed
Both canvases are always in the DOM, one displayedLosing the WebGL context swaps a display property and the next frame paints flat with score, wave and lock intact
The 3D view is lazily importedA visitor who never opens the arcade never fetches a byte of Three.js, and with the flat view as the default that is now almost everybody
The run always STARTS flatA slow import delays nothing and a failed one is invisible
No WebGL means flat, whatever the setting saysThe preference is a preference, not a requirement

The 3D view's own rules: every shape is generated in code (no model files, no textures), the glow is emissive material plus ACES tone mapping rather than a bloom pass (bloom is roughly thirteen fullscreen passes over eleven render targets, which is the exact bandwidth pattern that ruins tile-based mobile GPUs), a word is held at a constant fraction of viewport height however far away its craft is, craft are modelled to be read from ABOVE, and the camera distance is solved per resize rather than hard-coded.

7.12 Art, and what happens when it is missing#

The flat view draws generated sprites over its hand-drawn shapes, and the Arcade and Armory are staged with rendered backdrops. js/art.js is the one module that knows where any of it lives; everything else asks by key and never hard-codes a path. ART_DIRECTION.md is the companion document that says how the art itself was made and what it has to look like.

Two families, with two different compositing contracts:

FamilyWhat is in itHow it is drawn
HologramsGear icons, the galaxy, the chips and cores currenciesGlowing objects authored on pure black, composited additively (screen blend in the DOM, lighter on canvas), so they need no alpha channel and can never grow a halo
Solid craftThreat sprites, bosses, mini-bosses, hull renders, the player's baseReal alpha cutouts, because they occlude the starfield rather than adding to it

Vector fallback is law. Nothing that draws a sprite may assume it loaded: every caller checks spriteOk() and falls back to the hand-drawn shapes that shipped before any of this art existed. The app must play identically with assets/art/ deleted, the same way it already must play without webfonts and without Firebase. Every function in js/art.js is best-effort and silent, so a missing file downgrades the look and changes nothing else.

Two consequences worth knowing:

The weapon you are firing is visible. weaponVisual() derives the turret's look from what the weapon actually DOES rather than from its id: a splash weapon sprays, a combo feeder fires tracers, a precision weapon lances, and everything else fires the honest bolt. It is recomputed on every swap, so pressing Tab visibly changes the gun.

7.13 Music#

Four tracks, played by scene and crossfaded over 900 ms so a scene change is a dissolve rather than a cut:

SceneWhen
opsThe map room and every calm deck: the arcade hub, Training, Progress
hangarThe Armory
combatA live Starfall run
bossA boss or mini-boss encounter, back to combat when it resolves

Music is on by default at half volume, and both are in Settings. js/music.js creates no element and fetches no byte until music is enabled AND a scene is set AND the browser has seen a user gesture, so a visitor who never plays never pays for it. Every play() is best-effort: an autoplay refusal parks the intent and the first real gesture retries it, and nothing in the module throws, because a music failure must never cost a frame or a keystroke.

This is the one deliberate exception to "no audio assets". Keystroke-rate SOUND EFFECTS stay synthesized in js/audio.js, because they fire from keydown handlers and must never wait on a decode. Music is long-form and streamed, so files are the right tool for it and the wrong tool for a keypress.

8. Encounters: events, mini-bosses and bosses#

A wave counter that only ever spawns faster is a difficulty curve, not a rhythm. Encounters are the rhythm: three tiers of escalation that interrupt the ordinary run at different scales, each worth more than the minute before it.

8.1 The director#

makeDirector() decides what fires and when. It owns no rendering and no spawning: the game asks it for numbers and draws the banner itself.

RuleValue
Never during a boss waveA boss IS the event
Never two at onceOne encounter at a time
CooldownAt least 3 waves since the last encounter
Guaranteed introductionWave 3 always fires one
Otherwiseclamp(0.22 + wave * 0.02, 0.22, 0.55) chance per wave
Mini-bossesOnly from wave 6, with a rising share of the pool
WaveChance an encounter firesMini-boss share of the pool
3guaranteed0%
634%15%
1042%24%
1348%32%
1654%39% (capped)
17 and up55% (capped)39%

Every choice is drawn from the run's seeded generator, so two players in a Versus room meet the same events on the same waves with no server dealing them. Nothing in js/games/events.js reads Math.random.

8.2 Events#

Short and frequent. A modifier plus a reward multiplier. The spawn block is a patch applied over the game's own numbers for the duration, so the game never branches on an event id.

EventDurationXP multChip multWeightWhat it does
Meteor Shower25 s2.0x1.5x10Asteroids only, at 0.45x the spawn gap and 1.05x speed. Every word is built from your weakest keys
Ion Storm22 s2.0x1.4x9Everything descends 35% faster, 0.8x spawn gap
Blackout20 s2.5x1.6x7Words render as their first letter until you commit to them. 0.9x speed
Swarm20 s1.8x1.4x10Drones only, 0.4x spawn gap, words at 60% length
Blitz15 s2.4x1.8x6Everything at once: 0.35x spawn gap, 1.2x speed
Precision Protocol25 s2.2x1.5x8Words 25% longer, a mistype costs DOUBLE heat, a clean word scores TRIPLE
Cargo Run28 s1.6x3.0x6A cruiser drifting through at 0.7x speed with a 60% longer word, and its own loot table

An event is CLEARED by surviving it: the modifier was the challenge.

8.3 Mini-bosses#

An event that fights back. One tougher craft with an escort, a small health bar and a SENTENCE to answer, on a clock. Kill it before the clock and the whole encounter pays; let it reach the base and it does not.

Mini-bossClockProfileLinesLength bandSpeedDamageEscortShield wordXP multChip multWeight
Raider30 sUnremarkable1short0.85x223 dronesnone2.0x1.8x10
Harvester34 sPlated2short0.60x282 asteroidsnone2.2x2.0x9
Warden32 sVolatile1medium0.70x262 dronesBREACH2.4x2.0x7

A mini-boss's escort and shield are attached to its first line only. Its transmissions run 38 to 63 characters at the short band, 171 to 195 at medium.

8.4 Bosses#

The milestone, every fifth wave (every fourth in the Rift). Named, multi-phase, with shields only a command word breaks and a health bar across the top.

A boss is not a wave with a health bar, and the difference is the text. An ordinary threat carries a WORD, a mini-boss a SENTENCE, a boss a TRANSMISSION that grows from one sentence to a whole paragraph by its last phase.

That changes the texture of play, which is the actual job of a boss. Waves are staccato (lock, burst, release, find the next target) and the skill under test is target acquisition. A transmission is sustained, with no switching and no hunting, and the skill under test is stamina and composure.

BossTitleProfileDamageDescentChipsPhasesCounter-pick brief
The SentinelOrbital GuardianUnremarkable400.34x1203Nothing special about its plating. The one that lets you find out what you like flying
Hive MindSwarm IntelligenceSwarming440.30x1603It never stops spawning. Bring splash, or a combo weapon and never drop it
DreadnoughtLine BreakerPlated520.26x2204Four plates and nothing but plates. Splash is wasted on it
The OracleCipher EngineVolatile560.28x2603It reads your mistakes. A forgiving weapon costs you less here than a sharp one

Phase tables. Each phase is a gate: an optional command word that drops the shield, then one transmission to strip the phase. The escort arrives with the phase.

BossPhase 1Phase 2Phase 3Phase 4
The SentinelLOCKON, short, 2 dronesOVERRIDE, medium, 3 dronesPURGE, long, 2 asteroids
Hive Mindno shield, short, 4 dronesSILENCE, medium, 5 dronesCOLLAPSE, long, 2 splitters, words from your weak keys
Dreadnoughtno shield, short, 2 dronesPLATE TWO, medium, 1 cruiserPLATE THREE, medium, 3 asteroidsCORE, long, 2 splitters
The OracleDECRYPT, short, 3 dronesPARSE, medium, 3 asteroidsEXECUTE, long, 2 cruisers

The Oracle also hides what it has not said yet, and that applies to what it sends as well as to what it says: its escorts arrive hidden.

Phase health is the length of the transmission that opens it, times the sector's toughness, floored at 8. With the starter weapon that is one character of damage each, so reading the passage out strips the plate exactly: the same honest baseline an ordinary word keeps.

A fumbled transmission costs a second, shorter one, exactly as a fumbled word costs a second engagement. Backspace is allowed, but a corrected character still counts against accuracy and the damage it never dealt is not refunded: only a character that landed correctly the first time it was answered ever deals damage, so retyping cannot deal it twice.

Transmission lengths, measured across all four bosses:

BandCharactersRoughly
short45 to 62One sentence
medium167 to 224Two or three sentences
long358 to 400A paragraph, about 50 seconds of sustained typing at 80 WPM

A slow typist meets the same boss saying shorter things. bandForWpm caps the band: under 30 WPM everything is short, under 55 WPM nothing exceeds medium, and only above that does a boss reach for a paragraph. A phase's own length is the ceiling, this is the floor a beginner is protected by, and it is the same adaptation the word tiers already make.

Your typing is the weapon. While a transmission is live the turret has no target list: every CHARS_PER_SHOT (6, scaled by the weapon's fire rate, minimum 2) correct characters fires a round that destroys whatever is closest to your base. Type well and the escorts never arrive. Stall, or spray mistakes, and your guns fall silent exactly when the screen is filling up. The answer to "the boss is overwhelming me" is always "type better", which is the answer this whole site is built to reward.

A boss that reaches your base deals its damage and leaves. It is not cleared, so it drops nothing: that is what gives the fight stakes past the health bar.

8.5 Vulnerability profiles#

The reason a loadout is a counter-pick rather than a preference. A weapon choice that is simply "how I like to play" is a personality quiz; a weapon choice that is right for the Dreadnought and wrong for the Hive Mind is a decision.

ProfileNameClean damageFumbled damageSplashPiercePrefers
standardUnremarkable1.0x1.0x1.0x1.0xnothing in particular
armouredPlated1.0x1.0x0.35x1.5xprecision
swarmSwarming1.0x1.0x1.9x0.7xpressure
volatileVolatile1.0x0.55x1.0x1.0xforgiveness

The profile belongs to whatever encounter owns the field, so the Dreadnought's plating shrugs off a Flak Battery even while you are shooting its escorts.

Nothing enforces the counter-pick. loadoutAdvice() tells you in one sentence whether what you are carrying suits what is coming, and bossSchedule() puts the rota on the pre-flight screen so you can choose before the run. Bringing the wrong thing makes a fight harder, never impossible, which is the same stance sectors take about power.

8.6 Laps: the roster cycles#

Bosses arrive in order and the roster repeats. Each full lap raises the boss:

What scales per lapHow
NameGains a Roman numeral (Dreadnought II)
Damage+22% per lap, compounding on the base
Chips+35% per lap
Escort size+1 craft per phase per lap
Phase healthUnchanged (it is the transmission's length)

So wave 25 is the Sentinel again but meaner, rather than a boss nobody has met.

8.7 What an encounter pays#

Two shapes of reward, and the difference matters.

A timed encounter pays a MULTIPLIER on the work done WHILE IT WAS LIVE. That is why the ledger counts characters PER ENCOUNTER rather than merely noting that one happened:

eventBonusXp = (xpMult - 1) * (charactersTypedUnderIt / 5) * paceFactor * accFactor * difficulty

A blackout you fought through pays; a blackout you hid from does not, and a game cannot inflate the bonus by idling under a banner because there is nothing to multiply.

A boss pays FLAT, because a boss fight can run a long time and paying it by the character would make the reward a function of how SLOWLY it was killed. The director's multiplier returns 1 during a boss for the same reason.

AwardXPNotes
eventCleared140Per event survived
miniBossKill320Per mini-boss killed before its clock
bossPhase90Per phase stripped, so a LOST fight still pays
bossKill700The milestone
bossFirstKill900Added the first time this particular boss goes down

Chips follow the same split and stay speed-blind: an encounter pays (chipMult - 1) * characters / 5 * 0.5, plus 18 per event cleared, 40 per mini-boss, and 120 + 40 * phases for a boss.

9. Gear, weapons and builds#

Nine mounts, seven categories, fifty-three items. The catalogue is SIDEGRADES, not a ladder: every top-end item is better at what it does and worse at something else. Remove the downsides and the build system collapses into a shopping list where the best item is simply the most expensive one, which tools/check-modules.mjs checks for.

9.1 The slots#

SlotNameWhat it decides
weaponPrimaryWhat a correct character is worth, clean and after a slip. The accuracy bargain
attachmentPrimary attachmentTunes the primary toward precision or toward forgiveness
weapon2SecondaryThe other answer, one Tab away. Fitting one costs a little score
attachment2Secondary attachmentTunes the secondary
hullHullHow much punishment the base takes before it is over
targetingTargetingHelp with the lock. Every point of help costs score
coolantCoolantHeat capacity and how fast it bleeds off. Fewer jams
reactorReactorOutput: score, salvage, and the pace of the field
moduleModuleOne utility of your choosing. The panic buttons live here

A fresh profile owns and wears the starter of every slot, and the second hardpoint defaults to EMPTY rather than to the starter weapon: a one-weapon build is the default and the second mount is something you choose to fit.

9.2 The stat sheet#

The game reads the aggregate, never the items, so a new stat is one entry here plus one use in the game.

StatBaseClamped toMeaning
dmgClean10.5 to 3.2Damage per correct character while this target is unfumbled
dmgFoul10.2 to 2Damage per correct character after a slip on this target
splash00 to 1.4Fraction of a kill's word length dealt to the nearest other threat
pierce10.4 to 2.6Damage multiplier against bosses and mini-bosses
comboDmg00 to 0.02Extra damage per point of combo, capped at +60%
fireRate10.5 to 2Multiplier on characters per shot in a transmission (lower is faster)
maxHp10040 to 400Base health
dmgMult10.35 to 1.4Multiplier on damage taken
regen00 to 30Health restored per wave cleared
heatCap10050 to 320Heat before the turret jams
heatDecay208 to 80Heat bled per second
comboKeep00 to 0.85Fraction of the combo kept through a miss
assists00 to 8Mistyped characters forgiven per wave before the lock drops
reveal00 to 4Extra seconds a word is readable before it is in range
fieldSlow10.55 to 1.2Multiplier on descent speed (below 1 is easier)
scoreMult10.75 to 1.45Score, and therefore the score boards
salvageMult10.8 to 1.6Chips and drop quantity
charges00 to 8Emergency charges
shield00 to 140One-off absorb at the start of a run

The two bold clamps are the ones that keep the central claim true. The floor on dmgFoul is the other important one: a fumbled target must always still be killable, or a slip with a Railgun would mean standing there typing forever, which is a punishment rather than a cost.

How stats combine. Plain numbers add. scoreMult and salvageMult multiply. dmgMult and fieldSlow are reductions (a +1 upgrade makes a 0.9 into a slightly stronger 0.886, never a larger number). comboKeep takes the best rather than stacking. Only the LIVE hardpoint's weapon and attachment contribute, so carrying two weapons is never a way to add their numbers together.

9.3 Weapons#

The accuracy axis, running from "devastating while clean, useless after a slip" at one end to "never punished, never spectacular" at the other. Read the pairs as bargains, not as a ladder.

WeaponCostRequiresStat changesWhat it feels like
Service RepeaterfreestarternoneOne character, one point of damage, clean or not. The honest baseline
Empty Hardpointfree(second mount only)noneNothing fitted. Lighter, quieter, and it scores the most
Longbarrel Cannon950clean +0.4, foul -0.4, pierce +0.25The first step toward precision
Scatter Array950clean -0.05, foul +0.15, splash +0.35, pierce -0.2Barely notices a mistake, never hits hard
Arc Welder1,700level 9clean +0.05, foul +0.05, heat decay +9, fire rate -0.1Steady and cool running. Still firing when the others jam
Flak Battery1,900level 10clean -0.1, foul +0.2, splash +0.6, pierce -0.35Wide, forgiving, blunt. Hopeless against armour
Lance2,000level 12clean +0.55, foul -0.55, pierce +0.35, fire rate +0.1A clean word kills two thirds of the way in
Chain Driver2,200level 15clean -0.2, foul -0.1, comboDmg +0.009Weak alone. Lives on unbroken streaks
Railgun3,200level 26clean +0.9, foul -0.75, pierce +0.5, fire rate +0.25, score x1.08The purist option, and the highest scoring weapon
Bulwark Repeater5 coreslevel 28clean +0.05, foul +0.3, splash +0.5, hull +20, score x0.95Forgives everything. The endurance build, finished
Nova Lance6 coreslevel 32clean +0.75, foul -0.6, pierce +0.6, splash +0.2A Lance that also throws sparks. The accuracy build, finished

What those numbers actually do. With no attachment and no upgrades:

WeaponCleanFumbledSplashPierceAxisPowerCharacters to kill a clean 7-letter wordFumbled
Service Repeater1.001.000.001.00Balanced077
Longbarrel Cannon1.400.600.001.25Precision3512
Scatter Array0.951.150.350.80Balanced387
Arc Welder1.051.050.001.00Balanced877
Flak Battery0.901.200.600.65Forgiving486
Lance1.550.450.001.35Precision3516
Chain Driver0.800.900.001.00Balanced098
Railgun1.900.250.001.50Precision6428
Bulwark Repeater1.051.300.501.00Balanced1576
Nova Lance1.750.400.201.60Precision11418

Read the last two columns together: that is the whole design. A Railgun kills a clean word in four characters and needs twenty eight after a slip. A Flak Battery needs eight either way. At a steady 40 WPM with no mistakes they kill the same drone on the same word, and the power ratings say they are the same class of item, because power is a statement about gear and not about the pilot.

9.4 Attachments#

One socket per hardpoint, and its whole job is to move the weapon along the axis. The obvious play is to double down (a Lance with a Focusing Lens); the interesting one is to correct (a Railgun with a Recoil Damper, which is still sharper than anything else and no longer catastrophic when you slip).

AttachmentCostRequiresStat changes
Empty Socketfreestarternone
Focusing Lens700clean +0.2, foul -0.15
Recoil Damper700foul +0.28, clean -0.1
Splitter Prism1,400level 11splash +0.3, clean -0.1
Overclock Coil1,500level 13fire rate -0.25, heat capacity -18
Match Actuator1,600level 14clean +0.35, foul -0.35
Ballast Weight1,600level 14foul +0.4, score x0.96
Armour Piercer1,800level 17pierce +0.4, splash -0.15
Gyro Mount1,900level 16assists +1, clean -0.1
Tracer Feed2,000level 19comboDmg +0.005
Resonator4 coreslevel 30clean +0.25, splash +0.2, fire rate -0.1

The Gyro Mount is the rule in miniature: assistance costs DAMAGE here, exactly as it costs SCORE in the targeting slot. The Resonator is the one attachment that asks for nothing back, which is why it is priced in cores.

9.5 Hulls#

HullCostRequiresStat changes
Standard Framefreestarternone
Reinforced Hull700hull +25
Bulwark1,500level 8hull +45, damage taken x0.9, score x0.97
Glasswing2,000level 14hull -25, score x1.14, salvage x1.1
Mender Frame2,200level 15hull +30, regen 9 per wave
Aegis Plating2,400level 18hull +55, damage taken x0.85, shield 30, salvage x0.94
Titan Frame6 coreslevel 30hull +90, damage taken x0.72, regen 6, shield 25, score x0.9

9.6 Targeting#

The assistance slot, and therefore the slot where the score penalty lives. An empty sight is the highest scoring option in the game, on purpose.

TargetingCostRequiresStat changes
Open Reticlefreestarternone. The highest scoring option in the game
Assist Module800assists +2, score x0.97
Combo Governor1,600level 10combo kept 50%, assists +1, score x0.95
Predictive Lock2,300level 20assists +3, reveal +1.2 s, score x0.93
Duelist's Reticle2,600level 24score x1.18, heat capacity -20
Oracle Sight5 coreslevel 28combo kept 75%, assists +4, reveal +2 s, score x0.88

A skilled typist deliberately flies with an Open Reticle because assists would cost ten percent of a score they do not need help earning. A learner takes every assist on offer and reaches content they could not otherwise see. Both are correct. That is what a build is.

9.7 Coolant, reactor and module#

CoolantCostRequiresStat changes
Stock Ventfreestarternone
Twin Radiator650heat decay +10
Cryo Loop1,500level 9heat capacity +45, decay +14
Run-Hot Manifold1,800level 13capacity -25, decay -4, score x1.15, salvage x1.08
Zero-Point Sink2,600level 22capacity +80, decay +26, score x0.96
ReactorCostRequiresStat changes
Standard Cellfreestarternone
Field Dampener1,200field speed x0.92, salvage x0.96
Overcharged Cell1,400level 7score x1.12, heat capacity -15
Salvage Magnet1,900level 12salvage x1.25, heat decay -3
Flux Capacitor2,800level 24field speed x0.88, score x1.1, salvage x1.1
Singularity Core7 coreslevel 34field speed x0.8, score x1.2, salvage x1.2, hull -20
ModuleCostRequiresStat changes
Empty Mountfreestarternone
Chaff Launcher9002 charges
Emergency Shield1,300shield 45
Time Dilator2,100level 16field speed x0.9, reveal +0.8 s, score x0.95
Charge Battery2,500level 214 charges, hull +15
Anchor Protocol4 coreslevel 26combo kept 60%, heat decay +8, score x0.94
Prospector's Rig5 coreslevel 30salvage x1.3, 2 charges, hull -15

9.8 Upgrades#

Every owned item goes to +5. Each level is worth UPGRADE_STEP (14%) of the item's base stats, and an item's DOWNSIDES scale too: a +5 Glasswing is even more fragile, which keeps a sidegrade a sidegrade at every level.

chips for the step from +n = round(basePrice * (0.45 + n * 0.35))
basePrice for a core item  = cores * 420
the last two steps (+4, +5) also cost 1 core each
ItemBase price+1+2+3+4+5Total chipsTotal cores
Assist Module8003606409201,2001,4804,6002
Lance2,0009001,6002,3003,0003,70011,5002
Titan Frame2,520 (6 cores)1,1342,0162,8983,7804,66214,4902

Upgrading a good item competes with buying the next one up rather than being strictly worse, and the core cost on the last two steps means the very best loadout still has a boss fight standing behind it rather than an afternoon of drills.

9.9 Power#

One number for "how kitted am I", the way an ARPG shows item level. It is compared against a sector's recommendation and shown on the loadout screen. A bare call rates the BETTER of the two hardpoints, because a player who brought the right weapon for half the fight is equipped for the place.

StatWeight per point above base
maxHp0.30
dmgMult-34
regen1.4
heatCap0.10
heatDecay0.55
comboKeep26
assists5.5
reveal6
fieldSlow-46
scoreMult16
salvageMult12
charges7
shield0.22
dmgClean15
dmgFoul13
splash10
pierce9
comboDmg220
fireRate-14

Survivability is weighted heaviest because that is what sector difficulty actually tests. dmgClean and dmgFoul are weighted almost equally on purpose: a Lance is only more powerful than a Scatter Array in the hands of somebody who stays clean, and if precision alone raised power the Rift would look gated behind a weapon rather than behind a decision.

9.10 Two hardpoints#

A loadout frozen for a whole run makes vulnerability profiles a gotcha: you meet the Dreadnought's plating with a Flak Battery in your hands and there is nothing to do but remember for next time. Carrying two weapons and swapping with Tab turns that into a decision made DURING the fight.

Three things stop it from being free:

CostValue
The swap drops your lockImmediately
Spin-upSWAP_MS 600 ms, during which damage ramps from SWAP_DAMAGE 0.5 back to 1
Fitting a second hardpoint at allscore x0.94 and heat capacity -10, whichever weapon is live

So the purist running one weapon still scores highest, and the interesting question is two specialists (answer everything, score less) against one specialist and an empty mount (answer one thing perfectly).

9.11 Gambits#

Opt-in handicaps, taken before a run, that pay a multiplier on XP and salvage. They stack multiplicatively. This is the pressure valve for a player who has out-geared their sector: rather than nerfing their gear, let them BET it.

GambitMultiplierWhat it removes
No Shield1.15xStart with no absorb, whatever your hull says
Empty Racks1.12xNo emergency charges
Hot Start1.18xEvery wave begins at half heat
Iron Discipline1.20xNo assists and no combo protection, whatever your targeting says
Paper Hull1.35xHalf the base health you would otherwise have
Sudden Death1.60xOne hit ends the run. Any hit

Gambits OVERRIDE gear rather than subtracting from it, so "no assists" means none regardless of what is bolted on. Taking all six multiplies rewards by 3.94x, and js/run.js clamps the applied multiplier to 4x.

9.12 Four builds that work#

Worked examples, because a stat table does not teach a build. Every number below is what loadoutStats() actually returns for that set of items at +0.

The Purist. Railgun, Match Actuator, empty second mount, Open Reticle, Glasswing, Run-Hot Manifold, Overcharged Cell, Empty Mount.

Damage2.25 clean, 0.20 fumbled (the floor). Four characters kill a clean seven-letter word
Survivability75 hull, no shield, no charges, no assists, heat capacity 60
EarningScore x1.45, the cap. Salvage x1.19
Power rating5

Power 5 in the Rift, which recommends 92. That is the point: this build is not under-geared, it is geared for a pilot who does not intend to be hit. Add gambits until it stops being easy.

The Long Haul. Bulwark Repeater, Recoil Damper, Titan Frame, Oracle Sight, Zero-Point Sink, Flux Capacitor, Charge Battery.

Damage0.95 clean, 1.58 fumbled. A slip costs almost nothing
Survivability225 hull, damage taken x0.72, 25 shield, 4 assists, 4 charges, 75% of the combo kept, 2 seconds of reveal, heat capacity 180, field at 0.88x
EarningScore x0.80, salvage x1.10
Power rating180

Nearly twice the Rift's recommendation, and it earns 45% less score per point. That is gear buying access, exactly as designed.

The Counter-picker. Lance with an Armour Piercer for the Dreadnought on the primary, Flak Battery with a Splitter Prism for the Hive Mind on the secondary, Combo Governor, Mender Frame.

Primary1.55 clean, 0.45 fumbled, pierce 1.75
Secondaryforgiving and wide, one Tab away
Survivability130 hull, 9 regen per wave, 1 assist, half the combo kept
EarningScore x0.89 (0.95 for the Governor, times 0.94 for the second mount)
Power rating44

The Prospector. Scatter Array, Recoil Damper, Aegis Plating, Assist Module, Cryo Loop, Salvage Magnet, Prospector's Rig, flown in the Rift.

Damage0.85 clean, 1.43 fumbled, 0.35 splash
Survivability140 hull, damage taken x0.85, 30 shield, 2 assists, 2 charges
EarningSalvage x1.53, which the Rift then multiplies by 2.2. Score x0.97
Power rating70

Cores still only come from bosses, so even the farming build is about surviving to fight more of them.

10. Loot and the economy#

10.1 Two currencies#

CurrencyWhere it comes fromWhat it buys
ChipsTime and consistency: every run, every contract, every duplicateMost of both catalogues, and every upgrade
CoresONLY bosses and mini-bosses. Nothing else in the game produces oneThe Archivist's stock, and the last two upgrade levels of anything

Cores being boss-only is what makes a boss structurally worth starting rather than merely worth more points, and it is what the Archivist's stock is priced in. A contract that any run could finish must never pay one for nothing: the two contracts that do pay cores ask for a boss, a wave 15, or three clean runs.

10.2 Rarity#

Rarity is DERIVED from what an item costs and what it asks of you, rather than being a field on all 56 cosmetic entries. That keeps one source of truth: if an item is expensive or gated behind level 40, it IS rare, and the two can never disagree.

RarityColourDerived fromDuplicate valueBase drop weight
Standard#A4B2D2free, no requirement20% of costnever dropped
Polished#38DFD2anything cheaper than the bands below25%58
Rare#8B7BFFlevel 10+, or 1,000+ chips30%30
Elite#F2B33Dlevel 22+, or 2,000+ chips, or any badge requirement35%10
Legendary#FF5D8Flevel 40+, or 3,500+ chips, or a badge plus 2,000 chips40%2

An item may override this by carrying its own rarity, which is how a cheap thing can be made special deliberately.

10.3 Drop tables#

rolls is how many cosmetic cards the source turns over; chips and cores are inclusive ranges; floor forces every roll to at least that rarity.

SourceCosmetic rollsChipsCoresFloorRarity weights (polished / rare / elite / legendary)
Event cleared010 to 260
Cargo Run cleared160 to 140078 / 20 / 2 / 0
Mini-boss killed145 to 900 to 162 / 28 / 9 / 1
Boss killed2140 to 2801 to 340 / 36 / 19 / 5
Boss killed for the FIRST time3220 to 4002 to 4rare30 / 40 / 24 / 6

All drop quantities are then multiplied by the sector's loot multiplier, the loadout's salvage multiplier and any gambit multiplier.

Only a CLEARED encounter drops. A boss that reached your base takes its salvage with it.

10.4 The three anti-frustration rules#

  1. A duplicate is never nothing. It converts to chips at the rarity's rate and the reveal card says so. "You got something you already have, and it was worth nothing" is the fastest way to make a loot system feel hostile. Rolling also PREFERS items you do not own, so the duplicate path is a fallback rather than the common case.
  2. A pity counter guarantees an Elite or better within PITY_AT (6) rolls, applied to the FIRST roll of a batch so a guarantee does not become three. It is re-derived from what was actually GRANTED, so a roll that found nothing to give does not quietly eat a step of protection.
  3. A first kill floors at Rare. The first time you beat something should never pay out worse than the tenth.

10.5 The vendors#

Three shopfronts over the two catalogues, because a single undifferentiated shop is a spreadsheet and a shop with a face is a place you visit.

VendorCurrencyStockPricing
The QuartermasterchipsEverything, alwaysList price
The SalvagerchipsFive items, rotating at local midnight25% off
The ArchivistcoresPrestige stock onlyCores, which only bosses drop

The Salvager's five are drawn from a generator seeded on the DATE, so everybody sees the same five things on the same day with no server dealing them, and the countdown to the restock is the reason to look in tomorrow.

purchase() is the one payment path: it takes payment and grants in one block with nothing between the two lines that can throw, because "paid, not granted" is the one failure a player would never forgive.

10.6 Cosmetics#

Five purely decorative slots, 56 items. Nothing here affects gameplay. "Does this affect the game" is answered by which file an item lives in (js/cosmetics.js or js/gear.js), so the two are never blurred.

A cosmetic gated behind level 40 is legal precisely because it is a RECORD of reaching level 40 and changes nothing about play.

Ships (12)

ShipCostRequiresRarity
Standard IssuefreeStandard
Ember900Polished
Jade Runner1,200Rare
Nightshade1,500Rare
Drifter1,800Rare
Crimson Lance2,000badge: a 100 comboLegendary
Gilded Arrow2,200level 15Elite
Bulwark2,600level 25Elite
Void Cutter3,200badge: wave 20Legendary
Solar Flarefreebadge: 90 WPMElite
PhantomfreeStarfall rank 10Polished
Wardenfreelevel 40Legendary

Trails (10)

TrailCostRequiresRarity
VapourfreeStandard
Running DarkfreeStandard
Ion Wake700Polished
Ember Wake900Polished
Jadewash1,100Rare
Goldstream1,400level 12Rare
Aurora1,600level 20Rare
Ghostlight2,400badge: ten flawless runsLegendary
Solar Windfreebadge: three bosses in one runElite
NovafreeStarfall rank 15Polished

Keycaps (10)

KeycapCostRequiresRarity
SlatefreeStandard
Cream600Polished
Carbon800Polished
Mint1,000Rare
Amber1,200level 10Rare
Rose1,200Rare
Dusk1,500level 18Rare
Beaconfreebadge: 70 WPMElite
Ghostfreebadge: no weak keysElite
Championfreelevel 50Legendary

Name plates (10)

PlateCostRequiresRarity
Standard PlatefreeStandard
Steel700Polished
Beacon Plate1,000Rare
Amber Plate1,300level 14Rare
Rose Plate1,300Rare
Dusk Plate1,600level 22Elite
Void Plate2,000badge: wave 30Legendary
Jade Platefreebadge: 30 curriculum starsElite
Duellist Platefreebadge: ten Versus winsElite
Gold Platefreelevel 60Legendary

Titles (14)

TitleRequires
No titlefree
Cadetfree
Quick Fingersbadge: 50 WPM
Surgeonbadge: 99% over 600 characters
Regularbadge: seven day streak
Wavebreakerbadge: wave 20
Boss Killerbadge: six bosses in one run
Scholarbadge: every curriculum node cleared
Night Owlbadge: a run between two and five in the morning
Centurionbadge: 110 WPM
Duellistbadge: fifty Versus wins
Marathonerbadge: thirty day streak
Collectorbadge: twenty thousand chips earned
Legendlevel 80

Every title is free: a title is a record of something, so charging for it would make it a purchase instead.

11. The Training Grounds#

The Training tab is two ladders stacked. The Academy on top teaches the keyboard; Flight School below it is the curriculum, which teaches speed. They are scored on deliberately different axes and neither gates the other.

11.1 The Academy#

"Slow is smooth, and smooth is fast."

Coverage and accuracy, never speed. A player who certifies the whole keyboard at 22 WPM has done exactly as well as one who did it at 90, because what is being measured is whether every key is under your fingers and whether you hit it correctly, which is the thing speed is built on top of later. There is no WPM, no latency and no time anywhere in js/academy.js. If you find yourself adding a speed term to that file, it belongs in js/skill.js.

Twenty one groups across four stages. The key pool is CUMULATIVE: a group's drill material may use its own new keys plus everything earlier in the ladder, which is why the drills turn into real words surprisingly early and why nothing already learned goes stale.

StageGroupKeys addedThe idea
Home KeysIndex Anchorsf jThe bumps. Find them without looking and everything else has an address
Home KeysMiddle Fingersd kOne step in from the anchors
Home KeysRing Fingerss lThe weakest fingers on the strongest row
Home KeysLittle Fingersa ;The outside edge
Home KeysThe Stretchg hThe index fingers reach inward. The hand does not move
Home KeysThe Whole Home Rowasdfghjkl;All ten mixed. The first real checkpoint
The Top Rowr and ur uUp, and back down to the bumps
The Top Rowe and ie iTwo of the most common letters in English
The Top Roww and ow oRing fingers up
The Top Rowq and pq pThe far corners. Everybody is slow here
The Top Rowt and yt yThe inner reach
The Top RowTop Row and HomeqwertyuiopTwo rows mixed. Most short English words are now reachable
The Bottom Rowv b n mv b n mThe index fingers go down
The Bottom Rowc and commac ,Middle fingers down
The Bottom Rowx and full stopx .Ring fingers down
The Bottom Rowz and slashz /The last two corners
The Bottom RowEvery Lettera to zThe whole alphabet in real words. The second checkpoint
The Whole BoardCapitalsA to ZShift with the opposite hand, never the same one
The Whole BoardPunctuation, . ' " ; : ! ? -What turns typed words into typed sentences
The Whole BoardThe Number Row1 to 0The row nobody practises, and the one that slows real work most
The Whole BoardSymbols!@#$%^&*()[]{}<>/=+_~ plus backslash, vertical bar and backtickThe last thing between you and the diploma

Drill material. Real words wherever the cumulative pool can spell them, because a beginner typing "dad" and "flask" is learning something a beginner typing "fjfj djdj" is not. Roughly half the items in a drill must contain one of the group's NEW keys, or a wide pool would quietly stop teaching the group it is named after. The earliest groups fall back to short alternating chunks, because there is no English word in "f j" alone. Nothing repeats twice in a row: a repeat reads as a stutter and stops testing the transition into the key.

Certification is judged from the SHARED per-key model, and that is what makes testing out honest:

BarValueWhy
keySamples20 pressesBefore the model is allowed an opinion about a key at all
keyAcc90%EVERY key in the group must clear this. The strict half
groupAcc95%One completed drill must have reached this
reps1At least one completed drill on the group

The per-key floor is what "completeness" means: not "the group averaged 95" but "there is no key in here you are still missing". And the evidence is read out of state.keys rather than kept locally, so a key drilled in the arcade, or measured by the Flight Check, counts exactly as much as one drilled here.

That is the whole route a proficient typist takes. The placement seeds real per-key data, testOutCandidates() lists every group whose keys already clear the bar, and one clean drill certifies each. They are through in a few minutes and into the arcade, where their time is better spent.

A beginner walks the ladder, and groupStatus().blockers tells them in plain words what is still missing ("3 keys not practised enough", "q, p below 90%", "a drill at 95% or better"), because a beginner should never read a bare "not certified".

Nothing here is gated and graduation unlocks nothing. Every group is startable on the first visit, in any order. Certifying one is worth 300 XP, the largest flat award outside a boss. Graduating the whole keyboard is worth 3,000 XP, the largest flat award anywhere, because that is the one big number a slow typist can reach on their first evening. What graduation produces is a diploma(): recognition, a date and a duration. Not a key.

11.2 Flight School: the curriculum#

Twenty nodes in five groups, an ordered walk rather than a set of locks. Every node is playable from minute one; what the walk provides is an ORDER and a recommendation, which is a different service from a gate.

GroupNodeMaterialCharactersSignpost pace
FoundationsHome Row Anchorshomeasdfjkl;18
FoundationsThe Home Rowhomeasdfghjkl;22
FoundationsReaching UphomeToptop row plus home25
FoundationsReaching Downlettersbottom row plus home28
FoundationsThe Whole Alphabetlettersa to z32
The whole keyboardCommon Wordscommon36
The whole keyboardCapitals and ShiftcapitalsA to Z38
The whole keyboardEveryday Punctuationpunct,.'";:!?-40
The whole keyboardThe Number Rownumbers123456789042
The whole keyboardSymbols and Bracketssymbolsshifted row, brackets, slashes45
Real textLonger WordscommonLong48
Real textSentencesprose52
Real textParagraphsprose56
Real textCodecode(){}[];=<>_.55
PrecisionClean Sweepcommon45
PrecisionYour Trouble Keysgenerated live from your model40
PrecisionAwkward Pairsgenerated from your worst bigrams44
SpeedSprint Burstcommonfifteen seconds flat out70
SpeedTwo Minute Cruiseprose74
SpeedThe Long Haulprosefive minutes without a fade78

The signpost pace is not a gate and not what stars are measured against. Stars come from beating your own baseline (section 4.2). The goal number is there so the card can say what this material is usually comfortable at, and so the recommender can tell that a 110 WPM typist has no business being sent to the home row (OUTGROWN, 1.3x measured speed).

The two adaptive nodes are never skipped. Your Trouble Keys and Awkward Pairs build their material from the model, so their difficulty tracks whoever is typing: a 120 WPM typist still has a weakest key and a pair their fingers argue about, and drilling those is exactly what is left to do at that speed.

12. Flight Check: the sprint#

One surface doing three jobs.

12.1 The three jobs#

  1. The sprint. A timed typing test, a quote, or a drill on your own weakest keys. It reports through finishRun() like every other game, and the two timed modes carry leaderboards.
  2. The drill surface. Every Academy lesson and every curriculum node runs here. The Training Grounds hands it an explicit word list plus a meta tag, and that tag rides into the RunResult so the funnel scores the right thing. An Academy drill leads its results screen with accuracy and the per-key breakdown rather than a WPM number, because that is what it was judged on.
  3. The placement. See section 4.5. It runs a 60 second sprint and then grants, seeds and reports, and it deliberately does NOT go through finishRun().

12.2 The modes#

ModeClockMaterialRanked
60 seconds60,000 msMixed everyday wordsYes
15 seconds15,000 msMixed everyday wordsYes
QuoteuntimedOne of 20 real passages, punctuation and capitals attachedNo
Weak keys45,000 ms46 chunks built from the keys the model says are letting you downNo

Why the timed passage is NOT adaptive, when everything else on the site is: a board where each player typed material chosen for them is a board that compares nothing. So the timed modes and the placement all draw the same mixed passage of everyday words. Personal material lives in the Weak keys mode and in the Training Grounds, where nothing is ranked against anybody else.

Passage length for a timed run is sized at 22 characters per second, which is 264 WPM: comfortably past the fastest human on record, so nobody can reach the end of it.

12.3 Reading and the caret#

Three lines are visible and the caret rides the second one, so there is always a line of what is coming next below it. A fast typist reads ahead instead of typing into a wall.

A gap longer than 2,000 ms is treated as somebody answering the door rather than a slow reach, and is not folded into a key's mean latency.

13. Versus#

Head to head racing, for signed-in players with a claimed screen name.

13.1 Why Firestore and not the worker#

Everything else on this site talks to a Cloudflare Worker whose store is KV: eventually consistent, which is exactly right for a leaderboard and exactly wrong for a race. Two people typing against each other need each other's progress inside a few hundred milliseconds and they need it PUSHED. Firestore gives both, on the identity pool the site already authenticates against, so Versus costs no new service.

Four collections, no composite indexes: typePlayers (a presence card per account), typeLobbies (the room code IS the document id, so a shared link is a direct lookup), typeGames and typeChats.

13.2 The two formats#

Nobody referees. Both formats are built from a SEED in the game document and every client builds the identical race from it through mulberry32.

FormatLengthBuilt from the seedScored on
Starfall90 sThe whole spawn schedule up front: 4 + min(9, wave) threats per 11 second wave, each with its word, entry position and descent ratePoints
Sprint340 characters, hard stop at 210 sThe same passageNet WPM

A threat's position is a pure function of the shared clock (spawn moment, descent rate) rather than something integrated frame by frame, so a phone that dropped ten frames is still looking at exactly the same sky as the laptop it is racing. A client that reloads mid-race meets everything that fell while it was away in one frame: those words really did land on its base, so they cost what they cost, but they are not drawn or sounded.

Each client grades itself, which is safe because there is nothing secret here: the word lists ship in js/words.js either way, so a server holding the answers would be guarding a door with no wall attached. What the design DOES guarantee is that nobody can write anybody else's numbers, and that is enforced by the Firestore rules (a seat is a key in a map and only its owner may touch it) rather than by good manners.

13.3 Race tuning#

ConstantValue
Room code5 characters from ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (no I, O, 0 or 1)
Seats per room6
Countdown5,000 ms
Starting hull (starfall format)100
Wave length (starfall format)11,000 ms
Threat damage7, or 13 for a long word
Seat heartbeat15,000 ms
A seat is not counted after46,000 ms of silence
Host considered gone after90,000 ms
Progress pushedEvery 500 ms, throttled
ChatLast 40 lines, 240 characters each

13.4 What a race never touches#

Not the skill model, not the curriculum, not state.games, not the Academy, not History, not a solo leaderboard, and it does not go through finishRun() at all. A race is a different measurement (a shared seed, other people's pressure, nothing chosen by the player) and letting it feed the same numbers would quietly make two things one.

The only account-level trace is state.mp:

FieldMeaning
playedRaces finished
wonRaces won, which requires at least one opponent
pointsRanking points: max(1, playerCount - yourPlace), or 1 for a solo race. Raw scores are not summed because a starfall score and a sprint WPM are different units
bankedThe last 40 game ids, so a reload cannot count a race twice

Four versus badges read that tally on the next completed SOLO run, and a win may post to the versus wins board if the player has opted into leaderboards.

13.5 The four reliability rules#

Each was a bug first on the sibling site, and every one is load bearing:

  1. A listener's first snapshot comes from the local cache and can be a version behind (a transaction never lands locally), so nothing destructive may act on a snapshot until vsFresh() says the server confirmed it.
  2. A dotted updateDoc CREATES the map it writes into, so every seat write goes through vsTouchSeat, a transaction that only writes a seat that still exists. Because that is a server read every fifteen seconds it doubles as the backstop for a wedged lobby listener.
  3. Firestore's error callback is TERMINAL, so every long-lived listener is attached through vsWatch, which re-opens with backoff and never gives up on a connection error (only on a rules one). Everything re-opens at once on visibilitychange and online, because backoff measured against a sleeping clock is the wrong shape.
  4. A result is banked exactly once per account, guarded by state.mp.banked and persisted.

14. The meta layer#

The loops below the run. Everything in js/run.js closes inside a single run; everything here is what makes tomorrow worth turning up for. Each piece is a named idea rather than decoration, and nothing in it is a lock: a contract you ignore costs you nothing, a broken streak takes nothing away that you already had, and ascension is opt-in.

14.1 Contracts#

The Zeigarnik effect: an unfinished, clearly stated task nags in a way an open-ended one does not. Three daily and one weekly, each a single sentence with a number in it, seeded from the date so everybody gets the same set with no server dealing them. The three dailies are drawn without replacement and never two that track the same thing, because three "type N characters" contracts is one contract wearing three hats.

Daily contractGoalChipsCores
Keep Typing1,200 correct characters120
Long Shift2,500 correct characters220
Three SortiesFinish 3 runs of anything140
Hold the LineReach wave 10 in Starfall180
Deep PatrolReach wave 15 in Starfall2601
BountyBring down a boss2001
Clearing WorkKill 2 mini-bosses170
Steady HandsFinish a run at 97% accuracy or better200
Smooth Is FastFinish 3 runs at 97% or better3001
UnbrokenReach a combo of 60190
In the ZoneSpend 45 seconds in the Zone240
Academy BusinessCertify an Academy key group220
Range TimeFinish 2 Academy or Training drills150
Storm ChaserClear 3 field events180
Time TrialFinish a Flight Check sprint130
Weekly contractGoalChipsCores
The Long Week15,000 correct characters9002
Boss HunterBring down 5 bosses1,1003
Into the DeepReach wave 25 in a single run1,0003
Academy TermCertify 4 key groups9002
Precision Week10 runs at 97% or better1,0002
Flow State5 minutes in the Zone1,2003

Wave and combo contracts take the BEST single run rather than a sum, which is what the wording promises. A finished contract waits to be CLAIMED: collecting a reward you can see is worth more than one that lands silently, and the claim is the reason to open the panel tomorrow.

14.2 The streak#

Loss aversion, deliberately blunted. A streak that shatters after one missed day punishes exactly the person who was ill, and they do not come back.

RuleValue
AdvancesOnce per local day, on any completed run
Insurance earnedOne token every 5 days kept, capped at 3
Insurance spentAutomatically, on the first day you miss
What insurance doesPreserves the streak at its current length rather than growing it
Daily bonusmin(200, 20 + days * 12) chips and min(500, 60 + days * 28) XP

The bonus flattens because a bonus that keeps growing turns a game into an obligation.

14.3 The comeback bonus#

The likeliest moment to lose somebody is the run after a long gap, so that run pays extra instead of showing them how much they missed. After 5 days away, the first completed run pays clamp(round(120 * min(days, 30) / 5), 120, 700) XP.

14.4 The Zone and ascension#

Covered in sections 7.7 and 4.7. Total time in the Zone accumulates in state.meta.zoneMsTotal and feeds the weekly Flow State contract.

15. Achievements#

Sixty two badges, each a single pure predicate over the state and the run that just finished. checkBadges() stamps state.badges itself, so a badge can never be granted twice, and badge XP lands on the ACCOUNT only, never on a game's own XP: a game rank is a measure of how much of that game you have played, not of how many account-wide milestones happened to fall while you were playing it.

Mileage

BadgeXPRequirement
First Flight100Finish your first run
Ten Sorties150Finish ten runs
Fifty Sorties300Finish fifty runs
Hundred Sorties500Finish a hundred runs
Five Hundred Sorties1,000Finish five hundred runs
Ten Thousand Keys200Press ten thousand keys
A Hundred Thousand Keys450Press a hundred thousand keys
A Million Keystrokes1,200Press a million keys

Levels

BadgeXPRequirement
Recruit100Level 5
Ensign150Level 10
Lieutenant250Level 20
Commander400Level 30
Ace700Level 50
Vanguard900Level 75
Grandmaster1,200Level 100

Speed

BadgeXPRequirement
Thirty Club120A run at 30 WPM or better
Fifty Club20050 WPM
Seventy Club32070 WPM
Ninety Club50090 WPM
Hundred and Ten750110 WPM
One Thirty1,100130 WPM. Professional pace

Accuracy

BadgeXPRequirement
Not One Miss250A run of real length with no mistyped character
Ten Times Flawless600Ten flawless runs banked
Surgical40099% accuracy over six hundred characters
Perfect Wave200Clear a whole wave without one mistyped key
Five Perfect Waves450Five such waves in one run
Steady Hand30097% accuracy for a full minute of sprint

The arcade

BadgeXPRequirement
Wave Five120Reach wave 5
Wave Ten220Reach wave 10
Wave Twenty450Reach wave 20
Wave Thirty800Reach wave 30. The words stop being words
Boss Down200Your first boss
Three Down350Three bosses in one run
Fleet Breaker700Six bosses in one run
Chain of 25120A 25 character combo
Chain of 50250A 50 character combo
Chain of 100500A 100 character combo
Chain of 200900A 200 character combo
On Fumes400Finish a run with a tenth of your base left
Not a Scratch500Reach wave 10 without losing any hull
Perfect Storm900Wave 10 with a hundred combo and not one mistyped key

Learning

BadgeXPRequirement
First Clear150Clear your first curriculum node
Ten Stars250Ten curriculum stars
Thirty Stars500Thirty stars
Full Sweep800Clear every node at least once
Perfect Curriculum1,200Three stars on every node
No Weak Links700Every key you type often at 95% or better
Old Enemy450Bring a key you used to miss constantly back to 97%
Flight Checked100Take the Flight Check and get placed

Everything else

BadgeXPRequirement
Quartermaster200Earn a thousand chips
Outfitter400Earn five thousand chips
Collector800Earn twenty thousand chips
Contender200Play ten Versus games
First Blood200Win your first Versus game
Ten Wins450Win ten Versus games
Fifty Wins900Win fifty Versus games
Three Day Streak200Play three days in a row
Seven Day Streak400Seven days in a row
Thirty Day Streak1,000Thirty days in a row
Full Roster500Finish a run of every game in the arcade
Night Owl300Finish a run between two and five in the morning
Second Wind400Come back after a month away and set a personal best on the first run

16. Leaderboards, accounts and identity#

16.1 The boards#

Nine boards, served by the shared Cloudflare Worker at /typing/board and /typing/score.

BoardGameScopeRanked on
Sprint, 60 secondssprint60net WPM
Sprint, 15 secondssprint15net WPM
Starfall, Cadetstarfallcadetpoints
Starfall, Pilotstarfallpilotpoints
Starfall, Acestarfallacepoints
Starfall, Nightmarestarfallnightmarepoints
Starfall, deepest wavestarfallwavewave reached, any difficulty
Account levelaccountlevellifetime XP
Versus winsversuswinsraces won

Strictly opt-in. Nothing is ever posted until the player ticks the box and picks a signature. A run must also be complete and plausible, and the worker holds that line independently.

Every post carries evidence: accuracy, correct characters, characters typed, duration, net WPM, and (when the game timed its keystrokes) the median gap between them, which is what tells a human hand from a script holding a key down. The one exception is the account level board, which is an account number rather than one run's work, so hanging a run's accuracy off it would put a meaningless percentage on the row.

One run can feed more than one board: a deep Starfall run is both a score and a survival record.

Never render un-escaped remote data. Every screen name goes through escapeHtml and every number through Number() before it is interpolated, because the worker is not the only thing that could answer that fetch.

16.2 One identity, three save files#

The same Firebase project authenticates Big World Quiz, Big Piano Quiz and Big Typing Quiz, so one account and one verified screen name work everywhere. Progress does NOT: this site syncs to its own collection.

SiteFirestore collection
Big World Quizusers/<uid>
Big Piano QuizpianoUsers/<uid>
Big Typing QuiztypingUsers/<uid>

Screen names come from the shared registry at /name on the worker, which is the source of truth. SN_NAME_RE and SN_RESERVED in js/state.js mirror the worker's rules and are mirrored a third time in big-piano-quiz; the worker will refuse anything they let slip.

Everything works signed out. Firebase is lazily imported on first use, the account area is hidden entirely when it is not configured, and a fully local profile loses nothing except cross-device sync and Versus.

17. Data model and persistence#

Everything the player owns lives in ONE localStorage blob under typingQuiz.v1, and the whole blob is what syncs to Firestore.

BranchContents
profilePlacement record: placed, placeOffered, placeWpm, placeAcc, placeLevel
xptotal, level, chips, cores, earnedChips
gearowned (id to level), equipped (slot to id), gambits
lootopened, pity, best, byRarity
metaContracts, streak, ascensions, lastRunAt, zoneMsTotal
gamesPer game: xp, rank, plays, timeMs, best by mode key, badges, bosses
keys, bigramsThe per-key model, capped at 140 and 400 entries
curriculumPer node: stars, plays, bestWpm, bestAcc, baseWpm
academyPer group: reps, chars, bestAcc, certified; plus graduated
skillruns, chars, ewmaWpm, ewmaAcc, bestWpm, bestAcc
badgesid to { at, n }
cosmeticsowned list and equipped per slot
historyNewest first, capped at 200 runs
settingsSound effects and their volume, music and its volume (on at 0.5 by default), strict mode, difficulty, sector, the on-screen keyboard, caret, motion, and render3d (OFF by default: the flat renderer is the site)
lbLeaderboard opt-in, signature, country
mpVersus tally and banked game ids

Three rules hold this together:

  1. normalizeState() is the only source of shape guarantees. Any path that replaces state wholesale (boot, cloud adopt, clear all data) runs it, it is safe to run twice over the same object, and every new field is added there as well as in freshState().
  2. saveState() is the ONE writer. It stamps updatedAt and pokes the cloud through a hook, so a mutation can never quietly skip the sync.
  3. The blob has to stay small, because it syncs. That is why History, keys, bigrams and banked race ids are all capped.

Per-identity slots. A signed-in account stores under typingQuiz.v1:u:<uid>, so two people sharing a browser do not share a save file, and signing out returns you to the local one.

18. For developers#

18.1 The shape of the repo#

No build step, no bundler, no test suite, no package.json. The deployable output IS the repo root, served as-is by Cloudflare Pages. There is exactly one third-party dependency and it is committed rather than installed: Three.js and troika-three-text under vendor/, bound by an import map in app.html and loaded only by the arcade's 3D renderer, lazily.

The one thing that is generated rather than hand-written is bible.html, and it is committed too, so nothing has to be run before a deploy. Media lives under assets/ (art in assets/art/, the four music tracks in assets/music/) and none of it is load-bearing: both are skins over a game that plays without them.

Three pages, sharing one design language and no stylesheet:

PageStylesheetScriptDepends on the app
index.html (front page)css/home.cssjs/home.jsNo. It imports nothing from js/, fetches nothing and touches no localStorage
app.html (the game)css/app.cssjs/app.js (ES modules)It is the app
bible.html (this document)css/bible.cssjs/bible.jsNo. Same isolation rule as the front page

The isolation is the point: a broken module cannot take down the landing page or the manual.

18.2 The module graph#

js/ is one module per concern, acyclic. js/app.js is the entry point: it imports every module, holds the router and init(), and wires the cross-module hooks. Nothing imports it back.

LayerModules
Pure helpersutil.js (imports nothing)
Statestate.js
Rulesxp.js, words.js, skill.js, academy.js, gear.js, loot.js, meta.js, vendors.js, achievements.js, cosmetics.js
Presentationart.js (the asset registry and its vector fallback), music.js (the four-track score)
Servicesaudio.js, cloud.js, board.js, nav.js
The funnelrun.js
Gamesgames/registry.js, games/starfall.js, games/starfall3d.js, games/sprint.js, games/events.js
Panelsmodes/arcade.js, modes/train.js, modes/progress.js, modes/ranks.js, modes/armory.js
Multiplayerversus.js
Shellapp.js

Because modules run in strict mode with their own scope, the few shared mutable bindings are reassigned through exported setters (setState, setCurrentStorageKey, setApplyingRemote, setCurrentTab), and the subsystem cycles are broken by hooks that app.js wires at init():

HookBreaks the cycle
setSyncHookso state.js never imports cloud.js
setIdTokenProviderso board.js never imports cloud.js
setAfterAuth, setBusyProbeso a remote copy is never applied on top of a live run
setSwitchTab, setPaintLevelChip, setRefresh*so a panel can drive the shell without importing app.js

18.3 The invariants#

These are the ones an edit breaks by accident. Each is stated as the thing that must remain true.

  1. finishRun() is the one funnel. It is the only writer of XP, chips, cores, the skill model, badges, gear grants, per-game bests, History, contracts and leaderboard submissions. If a game needs a reward it does not grant, add it there.
  2. Only complete runs count. abandonRun() is the single exception and it takes ONLY the keystrokes.
  3. normalizeState() owns the state shape, and saveState() is the one writer.
  4. #kbCatch is persistent and must never be rebuilt or re-parented. A phone raises its keyboard only for a focused input.
  5. Nothing is ever gated.
  6. The skill model is the only thing that decides word tiers.
  7. The Academy never scores speed.
  8. The placement and the Academy head start only ever RAISE.
  9. Gear must never multiply XP.
  10. Every strong gear item keeps a downside.
  11. Cores come from bosses and mini-bosses and nowhere else.
  12. The events ledger counts characters PER ENCOUNTER.
  13. Loot rolls only on CLEARED encounters.
  14. Cosmetics never affect gameplay.
  15. The seeded PRNG is what lets Versus work without a referee. Introducing a bare Math.random into a seeded path silently desynchronises a race.
  16. Never render un-escaped user data. The rule is about the PATH a string takes, not about who wrote it, so authored catalogue strings go through escapeHtml too.
  17. _headers names every origin the page may reach. A new external dependency is two edits, not one.
  18. Sound effects are synthesized, music is streamed, and the line between them is the point. Every effect is made at runtime in js/audio.js on one compressor bus, created on the first user gesture and never at import time, because a typing game fires a sound on every keystroke and a decoded buffer per hit would stall exactly the sound that must never stall. The four music tracks are real files, fetched only once music is on and a scene is set. Do not add an audio FILE for anything that fires from a keydown handler.
  19. The Google Fonts stylesheets must stay async. A pending cross-origin stylesheet blocks script execution, and this has shipped as a bug on a sibling site where it read as "every link is broken".
  20. A renderer reads the run and never writes to it, and the flat view may not be deleted: it is the default, the no-WebGL path and the context-lost path all at once.
  21. The art is a skin, never a requirement. Every sprite call is gated on spriteOk() with the hand-drawn shape behind it, and the game must play identically with assets/art/ deleted.
  22. vendor/ is pinned, version in the directory name, and an upgrade is its own deliberate commit.
  23. The registry is the only list of what games exist.

18.4 How to add things#

A new mini-game. Write a module under js/games/ exporting meta, start(host, opts), stop() and ideally isRunning() and snapshot(). Add one entry to js/games/registry.js. Report the result through finishRun(). Nothing about the panel, the router or the scoring funnel changes. A soon entry is an announcement and must read as one.

A new sprite. Drop the file under assets/art/ in the directory its prefix implies and it resolves through artPath() with no code change; a key that needs a different home goes in the SPECIAL map in js/art.js. Draw it through sprite()/spriteOk(), never through a raw Image, and leave the vector shape behind it working. ART_DIRECTION.md states what it has to look like.

A new threat. One entry in KINDS in js/games/starfall.js (radius, damage, word length range, kill score, spawn weight, first wave, and any of the weak, splits or drops behaviour flags). If it needs a behaviour none of those cover, add the flag next to them rather than branching on the id in the step loop.

A new gear item. One entry in GEAR in js/gear.js with slot, cost or cores, an optional req, and stats as deltas from BASE_STATS. Give it a downside, or tools/check-modules.mjs is entitled to fail. If it needs a stat that does not exist, add it to BASE_STATS, give it a clamp in loadoutStats(), a weight in POWER_WEIGHTS, and one use in the game.

A new event or boss. One entry in EVENTS, MINI_BOSSES or BOSSES in js/games/events.js, plus its lines in TRANSMISSIONS or MINI_LINES. A boss needs a profile from PROFILES and a brief explaining the counter-pick, because the pre-flight screen shows both. Never read Math.random in that file.

A new badge. One row in BADGES in js/achievements.js: an id, a name, a description, an XP value and a pure predicate. It is stamped automatically and can never be granted twice.

A new word tier. One entry in TIERS in js/words.js whose words stay strictly inside the tier's own chars, plus its place in TIER_ORDER. tools/check-modules.mjs asserts every tier the curriculum names exists.

18.5 Verifying a change#

Serve the repo over http (the file protocol will not do: a browser refuses a module script from an opaque origin) and drive it with Playwright against the pre-installed Chromium. Two checkers stand in for a test suite, and both exit non-zero on the first failure:

CommandWhat it proves
node tools/check-modules.mjsThe arithmetic the progression design rests on: skill out-scales gear, assistance costs score, a sector advises and never blocks, an abandoned run banks nothing, every curriculum tier exists in the lexicon, every Academy group builds real material, weapons are bargains rather than a ladder, both hardpoints behave
python3 tools/check-app.pyEvery module parses and imports, the router reaches every tab, a banked run moves the level and survives a reload, the placement only raises, both renderers work, all three pages have no page errors or missing files

Module bindings are not global, so page.evaluate(() => state) cannot reach them. Two doors exist instead: window.BTQ in js/app.js is the deliberate test seam (state(), finishRun, switchTab, openFlightCheck, games, nodes, skill(), level(), cloud(), probe3d()), read by nothing in the app, and localStorage.getItem('typingQuiz.v1') is the persistence path.

A 3D frame needs a different kind of check. A scene whose shader will not compile, whose camera is aimed at nothing, or whose material is fully transparent throws nothing and logs nothing, and renderer.info still cheerfully counts the draw calls it made for triangles nobody can see. So the renderer reads its own pixels back and the checker asserts that objects were actually drawn. Do not replace that with an assertion about draw calls: draw calls are exactly the thing that lies.

18.6 Deploying, and the build stamp#

Two deployables, and only one of them is in this repo:

DeployableWhere it livesWhere it goes
The static sitethis repo rootCloudflare Pages
The backend Workerthe big-world-quiz repoDeployed from there, serving all three sites

Adding or changing a /typing/* endpoint is a commit in that other repo. Neither deploy implies the other is live.

Two files in the repo root are read by Cloudflare Pages rather than by a browser, and neither is reproduced by a plain local serve, so both have to be tested by deploying them or by serving with an equivalent handler:

FileWhat it does
_headersSecurity headers and the Content-Security-Policy that names every origin the page may reach
_redirectsThe two renderer addresses, /3d and /2d

Every deploy is stamped. All three pages carry a short content hash in the footer, written by node tools/stamp.mjs and visible as plain text (no JavaScript involved, so it still reads when the JavaScript is the broken thing). It is a hash of the DEPLOYABLE OUTPUT rather than a git commit, because a stamp recording the current commit cannot be committed: writing it changes the tree, which changes the commit.

Before committing:

node tools/build-bible.mjs     # if GAME_BIBLE.md changed
node tools/stamp.mjs           # always
node tools/check-modules.mjs
python3 tools/check-app.py

Both checkers fail on a stale stamp, deliberately: a stamp somebody forgot to regenerate is worse than none at all, because it states confidently that a stale deploy is fresh.

18.7 This document's own pipeline#

GAME_BIBLE.md is the source. tools/build-bible.mjs renders it into bible.html, which is committed like everything else, so the site still has no build step at serve time. The generator owns the page shell, the contents sidebar and the anchors; css/bible.css and js/bible.js own the look and the search. node tools/build-bible.mjs --check exits non-zero when the page is out of date with the markdown, and tools/check-modules.mjs runs that check so a stale page cannot ship quietly, exactly as with the stamp.

19. Balance reference#

Quick answers to the questions balance arguments actually start with.

19.1 How long does a level take?#

Assuming continuous play at the stated pace and 95% accuracy, pace XP only (no flat bonuses, no events, no gambits):

Net WPMXP per minuteMinutes from level 1 to 10To level 20To level 42 (the placement cap)
25122921,3717,351
4036974572,450
609437175938
801841989479
1003111153284
130576629153

In practice a beginner reaches level 10 far faster than that table suggests, because flat bonuses dominate their early runs: a first run, a first day, a first node clear, a first Academy certification and a couple of wave milestones is over 1,000 XP before any pace XP is counted, and level 10 costs 3,508.

That gap is the design working. Read the two columns together: the expert's XP is almost all pace, the beginner's is almost all flat, and neither is being paid for something they did not do.

19.2 What does the placement save?#

A 100 WPM typist is placed at level 22, which is 20,398 XP, which is about 65 minutes of continuous play at their own pace. A 130 WPM typist is placed at level 30, which is 41,115 XP, or about 70 minutes at theirs. That is the whole argument for the Flight Check existing: without it, the first hour of an expert's time is spent re-proving something one minute already proved.

19.3 Does gear out-earn skill?#

No, and the margin is deliberately enormous.

RouteBest available multiplier on earning
Gear, score1.45x (hard clamp)
Gear, salvage1.60x (hard clamp)
Sector, salvage2.20x (the Rift)
Gambits, stacked3.94x
Ascension, per level1.06x each
Typing faster0.35x to 8x

tools/check-modules.mjs asserts that the skill ratio between 120 WPM and 40 WPM is more than three times the best gear multiplier, and it is: 4.4x against 1.45x.

19.4 How much does accuracy cost?#

Two separate penalties, applied in different currencies:

AccuracyXP kept (squared)What it costs in the arcade
100%100%Nothing. Clean words die early with a precision weapon
95%90%One in twenty targets survives its word and must be re-engaged
90%81%Heat is a constant background concern
85%72%Jams start happening in the middle of waves
70%49%Half the XP, and a combo that never reaches the Zone

20. Glossary#

TermMeaning
AssistA forgiven mistype. Spent before the lock is, refilled per wave, and it costs score
Axis (accuracy)Where a loadout sits between precision (huge clean, weak fumbled) and forgiveness (flat either way)
BandWhich length of transmission a boss phase uses: short, medium or long
ChipsThe soft currency. Time and consistency, not speed
CleanA target that has not been fumbled during this engagement
ComboConsecutive correct characters
CoreThe hard currency. Only bosses and mini-bosses produce one
EncounterAn event, a mini-boss or a boss. Anything the director stages
Fumbled (foul)A target you have mistyped on. It takes reduced damage for the rest of the engagement
GambitAn opt-in handicap that pays a reward multiplier
HardpointA weapon mount. There are two, swapped with Tab
HeatRises on mistypes. Full heat jams the turret
Jam1,150 ms during which the turret takes no input at all
LedgerThe per-encounter record of characters typed, used to score multipliers honestly
LockThe threat your keystrokes are currently going to
Pace factorThe superlinear speed term in the XP formula
PityThe counter that guarantees an Elite drop within six rolls
PowerOne number summarising a loadout, compared against a sector's recommendation
ProfileA vulnerability sheet: how an encounter resists splash, pierce and mistakes
RevealSeconds of extra legibility bought by gear, drawn as a strip above the field
SectorA difficulty band with its own loot weighting. Never a lock
StrictThe mistype rule that drops your lock. Automatic from Ace upward
TierA word pool, chosen from measured skill
TransmissionThe prose a boss broadcasts, which is what you type to strip a phase
The ZoneThe flow state, lit by a long clean streak, paid per second held

21. Constants appendix#

Every tuning number, with the file that owns it. If a value here disagrees with the code, the code is right.

Progression, js/xp.js

ConstantValue
MAX_LEVEL100
level costround(60 + 40 * level^1.28)
REF_WPM40
PACE_EXP1.35
pace clamp0.35 to 8
accuracy factor(acc/100)^2
PLACE_CAP42
per-game rank costround(120 + 70 * rank^1.15), cap 50
chipswords * 0.5 + minutes * 8, +10 at 97% accuracy, +25 on a best

The run funnel, js/run.js

ConstantValue
MAX_PLAUSIBLE_WPM260
MIN_RUN_MS2,000
gambit multiplier clamp1 to 4
loot multiplier clamp0.5 to 6

The arcade, js/games/starfall.js

ConstantValue
STEP / MAX_FRAME1/60 s / 0.25 s
VIEW_TOP / REVEAL_TOP / BASE_Y-0.04 / 0.12 / 0.93
SPEED_BASE / SPAWN_BASE0.026 heights per second / 2,900 ms
JAM_MS / JAM_RESET / HEAT_PER_MISS1,150 ms / 45% / 13
BREATHER_MS / COUNTDOWN_MS4,200 ms / 2,200 ms
COMBO_STEP / COMBO_CAP10 / 4x
ZONE_WINDOW40 keystrokes
SCORE_CHAR / SCORE_KILL / SCORE_PER_LETTER / SCORE_CLEAN2 / 12 / 6 / 30
SCORE_PHASE / SCORE_MINI / SCORE_BOSS / SCORE_WAVE400 / 600 / 1,500 / 100 per wave
SLOW_MS / FOCUS_MS / EMP_MS8,000 / 10,000 / 6,000 ms
MAX_PARTICLES260
PROGRESS_MS250 ms (how often a versus room hears from us)

Encounters, js/games/events.js

ConstantValue
BOSS_EVERY5 waves (4 in the Rift)
CHARS_PER_SHOT6
encounter cooldown3 waves
encounter chanceclamp(0.22 + wave * 0.02, 0.22, 0.55)
mini-bosses appearwave 6
lap scalingdamage +22%, chips +35%, escort +1 per lap
band capsshort under 30 WPM, medium under 55 WPM

Gear, js/gear.js

ConstantValue
MAX_UPGRADE / UPGRADE_STEP+5 / 14% per level
SWAP_MS / SWAP_DAMAGE600 ms / 0.5
SECOND_HARDPOINTscore x0.94, heat capacity -10
COMBO_DMG_CAP+60%
SKILL_POWER_PER_WPM1.2

Salvage, js/loot.js

ConstantValue
PITY_AT6 rolls
duplicate conversion20% to 40% of list price by rarity
first kill floorRare

The meta layer, js/meta.js

ConstantValue
INSURANCE_EVERY / INSURANCE_CAP5 days / 3 tokens
COMEBACK_AFTER_DAYS5
ZONE.enterCombo / enterAcc / graceMisses20 / 96% / 1
ZONE.scoreMult / xpPerSec1.25x / 3.2
ASCEND_AT / ASCEND_BONUSlevel 100 / +6% each

The Academy, js/academy.js

ConstantValue
CERT.keySamples / keyAcc / groupAcc / reps20 / 90% / 95% / 1
Groups / stages21 / 4

The skill model, js/skill.js

ConstantValue
MIN_KEY_MS / MAX_KEY_MS40 / 3,000 ms
KEY_INERTIA / KEY_ALPHA_MIN0.25 / 0.1
KEY_KEEP / BIGRAM_KEEP140 / 400
SKILL_ALPHA_MIN / RUN_FULL_MS0.12 / 30,000 ms
CLEAR_ACC / STAR_ACC_392% / 96%
STAR_STEPS1.05x, 1.12x, 1.22x of your own baseline
BASE_UP / BASE_DOWN / MIN_BASELINE0.35 / 0.06 / 8 WPM
MIN_KEY_SAMPLE / OUTGROWN12 presses / 1.3x

Versus, js/versus.js

ConstantValue
CODE_LEN / MAX_SEATS5 / 6
COUNTDOWN_MS / STARFALL_MS5,000 / 90,000 ms
SPRINT_CHARS / SPRINT_CAP_MS340 / 210,000 ms
HB_MS / AWAY_MS / HOST_GONE_MS15,000 / 46,000 / 90,000 ms
WAVE_MS (race) / START_HP11,000 ms / 100

Presentation, js/art.js, js/music.js and js/state.js

ConstantValue
Music crossfade900 ms
Music tracksops, hangar, combat, boss
Music defaulton, at volume 0.5
settings.render3d defaultoff. The flat renderer is the site
Renderer addresses/3d and /2d, which redirect to app.html?3d=1 and ?3d=0
Sprite fallbackspriteOk(), with the hand-drawn shape behind every call

State, js/state.js

ConstantValue
STORE_KEYtypingQuiz.v1
HISTORY_CAP / KEY_CAP / BIGRAM_CAP200 / 140 / 400
TYPING_BACKEND_URLthe shared worker, or empty for fully local