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:
- A beginner needs short material, visible progress, forgiveness, and a reason to come back tomorrow before they are any good.
- An expert needs material that is actually hard, a number that moves fast enough to respect their speed, and no tutorial standing between them and the game.
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:
| Axis | What it measures | Shape | Who it favours |
|---|---|---|---|
| Level XP | Absolute typing speed | Superlinear (a 1.35 power of pace) | Fast typists, heavily |
| Mastery | Improvement against your own past | Ratio, relative to your own baseline | Everybody equally |
| Chips | Time spent and consistency | Linear in minutes, not speed-scaled | Slow, regular players |
| Gear | Access to harder content | Reduces failure, barely touches earning | Anyone who plays |
And two mechanisms remove the need to self-assess at all:
- The measured field. The arcade picks its word tier from the rolling skill model, so a beginner meets three-letter home row words and an expert meets code tokens on wave one of the same game.
- The placement. A 60 second Flight Check on the first visit grants the level that performance already deserves, so an experienced typist starts around level 22 instead of at the home row.
1.1 The rules the design will not break#
These are load bearing. Everything else is negotiable.
- 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.
- Gear buys access, skill buys rate. Gear reduces failure so you survive deeper. It never multiplies XP.
- 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.
- 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.
- 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.
- One funnel.
finishRun()injs/run.jsis 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#
- 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.
- 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.
- 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.
- 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#
| Where | Key | What it does |
|---|---|---|
| Arcade | any printable character | Locks the lowest threat whose word starts with it, then types into that lock |
| Arcade | Tab | Swaps to the other weapon hardpoint (only if one is fitted) |
| Arcade | Ctrl + Space | Spends an emergency charge on whatever is closest to your base |
| Arcade | Escape | Drops the current lock, at no cost |
| Arcade | any key during a wave card | Skips the breather and starts the next wave |
| Boss transmission | Backspace | Corrects a character (desktop only) |
| Sprint and drills | Backspace | Corrects a character. It restores the text but never refunds the accuracy |
| Anywhere | the tab bar | Six 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#
| Readout | Meaning |
|---|---|
| Hull | Base health. At zero the run ends and is scored |
| Heat | Rises on every mistype, bleeds off over time. Full means the turret jams for just over a second |
| Combo | Consecutive correct characters. Drives the score multiplier and the sound |
| Wave | The wave counter. Every fifth is a boss (every fourth in the Rift) |
| Score | What the leaderboards rank. Multiplied by combo, the Zone, FOCUS, your loadout and the difficulty |
| Charges | Emergency charges left, if your module carries any |
| Guns | Both hardpoints, with the live one lit |
2.4 Ten things worth knowing early#
- Typing the first letter of a word locks the LOWEST threat carrying it. The lowest one is the one about to land.
- A wrong key never deducts anything. It costs heat, the combo, and (in Strict mode) the lock.
- Power-ups are typed like anything else. They are the short words in capitals.
- 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.
- Being accurate with a precision weapon kills a word before you finish typing it. That saved time is the reward.
- Fumbling with a precision weapon means the target survives and takes a fresh short word. Nothing is deducted, you just meet it twice.
- 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.
- Cores only ever drop from bosses and mini-bosses. Nothing else in the game produces one.
- The Academy never measures speed. Certifying the whole keyboard is the largest single XP award in the game.
- 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:
| Loop | Length | What closes it |
|---|---|---|
| The keystroke | Under a second | A correct character: a rising pitch, a beam, damage |
| The wave | About a minute | Threats cleared, a wave card, a score milestone, an event banner |
| The encounter | Minutes | A boss killed and salvage falling out of it |
| The session | Twenty minutes to an hour | Levels, certifications, badges, a vendor's stock |
| The long game | Weeks | The 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 WPM | paceFactor | XP per minute at 100% | at 95% |
|---|---|---|---|
| 15 | 0.350 (floor) | 5 | 5 |
| 20 | 0.392 | 8 | 7 |
| 25 | 0.530 | 13 | 12 |
| 30 | 0.678 | 20 | 18 |
| 40 | 1.000 | 40 | 36 |
| 50 | 1.352 | 68 | 61 |
| 60 | 1.729 | 104 | 94 |
| 70 | 2.129 | 149 | 134 |
| 80 | 2.549 | 204 | 184 |
| 90 | 2.988 | 269 | 243 |
| 100 | 3.445 | 345 | 311 |
| 110 | 3.918 | 431 | 389 |
| 120 | 4.407 | 529 | 477 |
| 130 | 4.910 | 638 | 576 |
| 140 | 5.426 | 760 | 686 |
| 160 | 6.498 | 1040 | 938 |
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:
| Bonus | XP | When |
|---|---|---|
firstRun | 250 | The first completed run of a game |
personalBest | 150 | A new best for this game and mode |
dailyFirst | 200 | The first completed run of the day, any game |
perfectRun | 220 | 100% accuracy on a run of at least 60 characters |
waveMilestone | 60 | Per multiple of 5 waves reached in the arcade |
nodeFirstClear | 200 | First time a curriculum node is cleared |
nodeStar | 120 | Each new star on a curriculum node |
academyCertified | 300 | Each Academy key group certified |
academyGraduated | 3000 | Certifying 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.
| Level | Cost of the next level | Total XP to reach |
|---|---|---|
| 1 | 100 | 0 |
| 2 | 157 | 100 |
| 5 | 374 | 776 |
| 10 | 822 | 3,508 |
| 20 | 1,911 | 16,457 |
| 30 | 3,170 | 41,115 |
| 42 | 4,844 | 88,209 |
| 50 | 6,041 | 131,112 |
| 60 | 7,613 | 198,527 |
| 80 | 10,975 | 382,270 |
| 100 | (capped) | 635,670 |
Ranks follow the level, so the number and the word never disagree:
| Level | Rank |
|---|---|
| 1 | Cadet |
| 5 | Recruit |
| 10 | Ensign |
| 15 | Pilot |
| 20 | Lieutenant |
| 30 | Commander |
| 40 | Captain |
| 50 | Ace |
| 65 | Wing Commander |
| 80 | Vanguard |
| 95 | Legend |
| 100 | Grandmaster |
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):
| Rule | Value |
|---|---|
| Accuracy needed to clear the node at all | 92% |
| First cleared run | Earns one star by definition, and becomes the baseline |
| Further stars | Land at 1.05x, 1.12x and 1.22x of your own baseline for that node |
| Third star also requires | 96% accuracy |
| Stars are never taken away | Once awarded, permanent |
| Baseline rise on a better run | 35% of the gap, immediately |
| Baseline fall on a worse cleared run | 6% of the gap |
| Minimum baseline | 8 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:
| Session | Chips | XP |
|---|---|---|
| 5 minutes at 25 WPM | 103 | 61 |
| 5 minutes at 120 WPM | 340 | 2,437 |
| 20 minutes at 25 WPM | 410 | 244 |
| 20 minutes at 60 WPM | 760 | 1,912 |
| 20 minutes at 120 WPM | 1,360 | 9,747 |
| 60 minutes at 25 WPM | 1,230 | 733 |
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 WPM | Level at 100% | at 97% | at 92% | XP granted to a new account at 97% |
|---|---|---|---|---|
| 20 | 1 | 1 | 1 | 0 |
| 30 | 3 | 3 | 3 | 257 |
| 40 | 6 | 6 | 5 | 1,150 |
| 50 | 9 | 8 | 8 | 2,149 |
| 60 | 12 | 11 | 10 | 4,330 |
| 70 | 14 | 14 | 13 | 7,400 |
| 80 | 17 | 16 | 15 | 9,973 |
| 90 | 20 | 19 | 18 | 14,664 |
| 100 | 23 | 22 | 20 | 20,398 |
| 110 | 26 | 24 | 23 | 24,822 |
| 120 | 28 | 27 | 25 | 32,392 |
| 130 | 31 | 30 | 27 | 41,115 |
| 160 | 39 | 38 | 35 | 70,275 |
| 180+ | 42 (cap) | 42 | 40 | 88,209 |
Four rules make it safe to re-take:
- It only ever raises. The grant is the difference between what you have and what the measurement is worth, floored at zero.
- 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.
- 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.
- 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 rank | Cost of the next | Cumulative |
|---|---|---|
| 1 | 190 | 0 |
| 5 | 566 | 1,298 |
| 10 | 1,109 | 5,191 |
| 20 | 2,314 | 21,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.
| Rule | Value |
|---|---|
| Must be complete | complete: false is discarded everywhere |
| Minimum duration | 2,000 ms |
| Must have typed something | typed > 0 |
| Correct cannot exceed typed | enforced |
| Maximum net WPM | 260 |
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.
| Parameter | Value | Why |
|---|---|---|
MIN_KEY_MS | 40 ms | Faster than this is a paste or a stuck key, not a finger |
MAX_KEY_MS | 3,000 ms | Slower is somebody answering the door |
KEY_INERTIA | 0.25 | Old evidence is discounted, so the model notices improvement |
KEY_ALPHA_MIN | 0.1 | A veteran's record stays movable |
KEY_CAP | 140 characters | The blob syncs and has to stay small |
BIGRAM_CAP | 400 pairs | Same |
MIN_KEY_SAMPLE | 12 presses | Before a key may be called weak |
ACC_FLOOR | 70% | As bad as the accuracy scale goes for one key |
LAT_SLOW / LAT_FAST | 1.8x / 0.6x | Of your own average pace, scoring 0 and 100 |
DEFAULT_REF_MS | 320 ms | Stand-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.
| Tier | Name | Characters | Words | Chosen at |
|---|---|---|---|---|
home | Home row | asdfghjkl; | 69 | under 18 WPM |
homeTop | Home and top rows | + qwertyuiop | 411 | 18 to 27 |
letters | Every letter | a to z | 332 | 28 to 37 |
common | Common words | a to z | 681 | 38 to 51 |
commonLong | Longer words | a to z | 767 | 52 to 67 |
capitals | Capitals | + A to Z | 201 | 68 to 84 |
punct | Punctuation | + ,.;:!?()-'" | 166 | 85 to 99 |
numbers | Numbers | + digits and .:/-,$% | 127 | (by node) |
symbols | Symbols | letters, digits, @#$%^&*_+=/<>~-! plus the vertical bar and the backtick | 118 | (by node) |
code | Code | everything | 183 | 100+ |
prose | Prose | sentences with punctuation | 371 | (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:
- asteroid chunks in the arcade, which are the one thing in the game that is not a real word, because there is no English word made of the four keys you are worst at,
- the Meteor Shower event, which is built entirely from them,
- the Hive Mind boss, which speaks in them on its last phase,
- the sprint's Weak keys mode and the curriculum's two adaptive nodes.
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.
| Tab | Id | What is there |
|---|---|---|
| Arcade | arcade | The 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 |
| Training | train | The Academy on top, the curriculum (Flight School) below |
| Versus | versus | Head to head racing, signed in with a claimed name |
| Progress | progress | What is open today, how good you are now, what you have done |
| Leaderboards | ranks | Eight opt-in boards |
| Armory | armory | Staged 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.
| Game | Id | Status | Modes | Board scope |
|---|---|---|---|---|
| Starfall Keys | starfall | Live | One mode (Endless), four difficulties | Difficulty, plus a deepest-wave board |
| Flight Check | sprint | Live | 60 seconds, 15 seconds, Quote, Weak keys | The two timed modes |
| Wing Command | coop | Announced | Pair, Squadron | (none yet) |
| The Forge | forge | Announced | Worst five, Transitions, The ladder | (none yet) |
| Terminal | terminal | Announced | Tokens, 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.
| Constant | Value | Meaning |
|---|---|---|
STEP | 1/60 s | Fixed timestep, with an accumulator, so a 144Hz laptop and a throttled phone run the same simulation |
MAX_FRAME | 0.25 s | The most a slept tab may catch up in one frame |
VIEW_TOP | -0.04 | The sliver above the field a craft drifts in from |
REVEAL_TOP | 0.12 | The deepest strip the reveal stat can buy |
BASE_Y | 0.93 | A threat at or past this has arrived |
MAX_PARTICLES | 260 | Decoration 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.
| Threat | Radius | Damage on arrival | Word length | Kill score | Spawn weight | From wave | Special behaviour |
|---|---|---|---|---|---|---|---|
| Drone | 15 | 6 | 3 to 5 | 0 | 10 | 1 | The baseline threat. Nothing special |
| Cruiser | 27 | 13 | 8 to 12 | 40 | 3 | 3 | Slow and long. Drops a power-up 85% of the time |
| Asteroid | 21 | 9 | 3 to 6 | 25 | 4 | 2 | Its "word" is a chunk built from YOUR weakest keys |
| Splitter | 19 | 7 | 5 to 7 | 20 | 3 | 4 | On death by typing or splash it becomes two drones at 1.35x speed |
| Power-up | 13 | 0 | the capital word | none | (dropped) | any | Falls 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#
| Rule | Behaviour |
|---|---|
| Acquiring | The 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 locked | Every later keystroke belongs to that lock. There is no target switching until the lock ends |
| Committing reveals | A hidden word (Blackout, the Oracle) shows itself the moment you lock it |
| A wrong key | Costs heat and the combo, and marks the target as fumbled for the rest of this engagement |
| Assists | Spent BEFORE the lock is. An assist forgives one wrong character, works in Strict mode too, and refills at the start of each wave |
| Strict mode | Drops the lock on a wrong key once assists are gone |
| Forgiving mode | Keeps the lock. The mistake still costs heat, accuracy and the combo |
Escape | Drops the lock deliberately, at no cost, so you are never stuck typing three wrong characters to shake off a target |
| The word running out | If 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#
| Constant | Value |
|---|---|
| Heat per mistype | 13, times any event heat multiplier |
| Heat capacity | 100 base, heatCap from gear (clamped 50 to 320) |
| Heat decay | 20 per second base, heatDecay from gear (clamped 8 to 80) |
| Jam duration | 1,150 ms |
| Heat left after a jam | 45% of capacity |
| Wave start | Heat 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:
| Event | Base points |
|---|---|
| A correct character | 2 |
| A kill | 12 + (word length x 6) + the kind's own score |
| A word killed with no slip at all | 30, times any event perfect multiplier |
| A boss phase stripped | 400 |
| A mini-boss destroyed | 600 |
| A boss destroyed | 1,500 |
| A wave cleared | 100 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:
| Parameter | Value |
|---|---|
| Combo needed to light it | 20 |
| Recent accuracy needed | 96%, over a window of the last 40 keystrokes (at least 12) |
| Misses tolerated before it drops | 1 |
| Score while lit | x1.25 |
| XP per second held | 3.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.
| Word | Effect | Duration |
|---|---|---|
NUKE | Destroys every threat on screen except bosses and other power-ups | instant |
SHIELD | Restores 22% of maximum hull and adds 20 shield | instant |
SLOW | The whole field descends at half speed | 8,000 ms |
FOCUS | Double score | 10,000 ms |
EMP | Spawning is frozen | 6,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:
| Difficulty | XP and score multiplier | Descent speed | Strict by default |
|---|---|---|---|
| Cadet | 0.8x | 0.75x | No |
| Pilot | 1.0x | 1.0x | No |
| Ace | 1.3x | 1.3x | Yes |
| Nightmare | 1.7x | 1.7x | Yes |
Sectors are where gear turns into access. Every one is enterable at any power: the recommendation is advice, not a lock.
| Sector | Recommended power | Speed | Damage taken | Salvage | Cores | Boss every |
|---|---|---|---|---|---|---|
| Home Sector | 0 | 1.00x | 1.00x | 1.00x | 1.0x | 5 waves |
| The Belt | 26 | 1.14x | 1.10x | 1.25x | 1.2x | 5 waves |
| Deep Field | 58 | 1.32x | 1.25x | 1.60x | 1.5x | 5 waves |
| The Rift | 92 | 1.55x | 1.45x | 2.20x | 2.2x | 4 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():
| Field | Contents |
|---|---|
game, mode | starfall, and the board scope (the difficulty, or versus) |
complete | Whether the run ended properly |
durationMs, typed, correct, wrong, wpm, netWpm, acc | The plain measurements |
keys | The per-character and per-bigram tally, with timings |
score, wave, combo, difficulty | The arcade extras |
meta.events | The encounter ledger: what fired, whether it cleared, its multipliers, characters typed under it |
meta.zoneMs | Milliseconds held in the Zone |
meta.sector, meta.lootMult, meta.gambitMult | What was flown and what was bet |
meta.medianMs | Median 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.
| URL | What runs |
|---|---|
bigtypingquiz.com | Flat, the shipped game |
bigtypingquiz.com/3d | 3D, in development. Redirects to app.html?3d=1#arcade |
bigtypingquiz.com/2d | Flat 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:
| Rule | Why |
|---|---|
| A view READS the run state and never writes to it | It is what makes them interchangeable rather than two forks of a game |
| Neither may draw from the run's seeded generator | A 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 displayed | Losing the WebGL context swaps a display property and the next frame paints flat with score, wave and lock intact |
| The 3D view is lazily imported | A 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 flat | A slow import delays nothing and a failed one is invisible |
| No WebGL means flat, whatever the setting says | The 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:
| Family | What is in it | How it is drawn |
|---|---|---|
| Holograms | Gear icons, the galaxy, the chips and cores currencies | Glowing 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 craft | Threat sprites, bosses, mini-bosses, hull renders, the player's base | Real 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:
- An asteroid picks one of two rocks from its own seed, so a field of them is not one rock stamped twenty times, and the pick is a pure function of the threat rather than a fresh roll (a roll would desynchronise a race).
- The player's craft is a cosmetic, so the sprite is tinted at runtime. Five neutral base renders are recoloured from the equipped cosmetic's own hull and trim hex and cached per combination, which keeps cosmetics purely cosmetic while still letting them change what you look at.
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:
| Scene | When |
|---|---|
ops | The map room and every calm deck: the arcade hub, Training, Progress |
hangar | The Armory |
combat | A live Starfall run |
boss | A 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.
| Rule | Value |
|---|---|
| Never during a boss wave | A boss IS the event |
| Never two at once | One encounter at a time |
| Cooldown | At least 3 waves since the last encounter |
| Guaranteed introduction | Wave 3 always fires one |
| Otherwise | clamp(0.22 + wave * 0.02, 0.22, 0.55) chance per wave |
| Mini-bosses | Only from wave 6, with a rising share of the pool |
| Wave | Chance an encounter fires | Mini-boss share of the pool |
|---|---|---|
| 3 | guaranteed | 0% |
| 6 | 34% | 15% |
| 10 | 42% | 24% |
| 13 | 48% | 32% |
| 16 | 54% | 39% (capped) |
| 17 and up | 55% (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.
| Event | Duration | XP mult | Chip mult | Weight | What it does |
|---|---|---|---|---|---|
| Meteor Shower | 25 s | 2.0x | 1.5x | 10 | Asteroids only, at 0.45x the spawn gap and 1.05x speed. Every word is built from your weakest keys |
| Ion Storm | 22 s | 2.0x | 1.4x | 9 | Everything descends 35% faster, 0.8x spawn gap |
| Blackout | 20 s | 2.5x | 1.6x | 7 | Words render as their first letter until you commit to them. 0.9x speed |
| Swarm | 20 s | 1.8x | 1.4x | 10 | Drones only, 0.4x spawn gap, words at 60% length |
| Blitz | 15 s | 2.4x | 1.8x | 6 | Everything at once: 0.35x spawn gap, 1.2x speed |
| Precision Protocol | 25 s | 2.2x | 1.5x | 8 | Words 25% longer, a mistype costs DOUBLE heat, a clean word scores TRIPLE |
| Cargo Run | 28 s | 1.6x | 3.0x | 6 | A 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-boss | Clock | Profile | Lines | Length band | Speed | Damage | Escort | Shield word | XP mult | Chip mult | Weight |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Raider | 30 s | Unremarkable | 1 | short | 0.85x | 22 | 3 drones | none | 2.0x | 1.8x | 10 |
| Harvester | 34 s | Plated | 2 | short | 0.60x | 28 | 2 asteroids | none | 2.2x | 2.0x | 9 |
| Warden | 32 s | Volatile | 1 | medium | 0.70x | 26 | 2 drones | BREACH | 2.4x | 2.0x | 7 |
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.
| Boss | Title | Profile | Damage | Descent | Chips | Phases | Counter-pick brief |
|---|---|---|---|---|---|---|---|
| The Sentinel | Orbital Guardian | Unremarkable | 40 | 0.34x | 120 | 3 | Nothing special about its plating. The one that lets you find out what you like flying |
| Hive Mind | Swarm Intelligence | Swarming | 44 | 0.30x | 160 | 3 | It never stops spawning. Bring splash, or a combo weapon and never drop it |
| Dreadnought | Line Breaker | Plated | 52 | 0.26x | 220 | 4 | Four plates and nothing but plates. Splash is wasted on it |
| The Oracle | Cipher Engine | Volatile | 56 | 0.28x | 260 | 3 | It 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.
| Boss | Phase 1 | Phase 2 | Phase 3 | Phase 4 |
|---|---|---|---|---|
| The Sentinel | LOCKON, short, 2 drones | OVERRIDE, medium, 3 drones | PURGE, long, 2 asteroids | |
| Hive Mind | no shield, short, 4 drones | SILENCE, medium, 5 drones | COLLAPSE, long, 2 splitters, words from your weak keys | |
| Dreadnought | no shield, short, 2 drones | PLATE TWO, medium, 1 cruiser | PLATE THREE, medium, 3 asteroids | CORE, long, 2 splitters |
| The Oracle | DECRYPT, short, 3 drones | PARSE, medium, 3 asteroids | EXECUTE, 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:
| Band | Characters | Roughly |
|---|---|---|
| short | 45 to 62 | One sentence |
| medium | 167 to 224 | Two or three sentences |
| long | 358 to 400 | A 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.
| Profile | Name | Clean damage | Fumbled damage | Splash | Pierce | Prefers |
|---|---|---|---|---|---|---|
standard | Unremarkable | 1.0x | 1.0x | 1.0x | 1.0x | nothing in particular |
armoured | Plated | 1.0x | 1.0x | 0.35x | 1.5x | precision |
swarm | Swarming | 1.0x | 1.0x | 1.9x | 0.7x | pressure |
volatile | Volatile | 1.0x | 0.55x | 1.0x | 1.0x | forgiveness |
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 lap | How |
|---|---|
| Name | Gains 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 health | Unchanged (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.
| Award | XP | Notes |
|---|---|---|
eventCleared | 140 | Per event survived |
miniBossKill | 320 | Per mini-boss killed before its clock |
bossPhase | 90 | Per phase stripped, so a LOST fight still pays |
bossKill | 700 | The milestone |
bossFirstKill | 900 | Added 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#
| Slot | Name | What it decides |
|---|---|---|
weapon | Primary | What a correct character is worth, clean and after a slip. The accuracy bargain |
attachment | Primary attachment | Tunes the primary toward precision or toward forgiveness |
weapon2 | Secondary | The other answer, one Tab away. Fitting one costs a little score |
attachment2 | Secondary attachment | Tunes the secondary |
hull | Hull | How much punishment the base takes before it is over |
targeting | Targeting | Help with the lock. Every point of help costs score |
coolant | Coolant | Heat capacity and how fast it bleeds off. Fewer jams |
reactor | Reactor | Output: score, salvage, and the pace of the field |
module | Module | One 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.
| Stat | Base | Clamped to | Meaning |
|---|---|---|---|
dmgClean | 1 | 0.5 to 3.2 | Damage per correct character while this target is unfumbled |
dmgFoul | 1 | 0.2 to 2 | Damage per correct character after a slip on this target |
splash | 0 | 0 to 1.4 | Fraction of a kill's word length dealt to the nearest other threat |
pierce | 1 | 0.4 to 2.6 | Damage multiplier against bosses and mini-bosses |
comboDmg | 0 | 0 to 0.02 | Extra damage per point of combo, capped at +60% |
fireRate | 1 | 0.5 to 2 | Multiplier on characters per shot in a transmission (lower is faster) |
maxHp | 100 | 40 to 400 | Base health |
dmgMult | 1 | 0.35 to 1.4 | Multiplier on damage taken |
regen | 0 | 0 to 30 | Health restored per wave cleared |
heatCap | 100 | 50 to 320 | Heat before the turret jams |
heatDecay | 20 | 8 to 80 | Heat bled per second |
comboKeep | 0 | 0 to 0.85 | Fraction of the combo kept through a miss |
assists | 0 | 0 to 8 | Mistyped characters forgiven per wave before the lock drops |
reveal | 0 | 0 to 4 | Extra seconds a word is readable before it is in range |
fieldSlow | 1 | 0.55 to 1.2 | Multiplier on descent speed (below 1 is easier) |
scoreMult | 1 | 0.75 to 1.45 | Score, and therefore the score boards |
salvageMult | 1 | 0.8 to 1.6 | Chips and drop quantity |
charges | 0 | 0 to 8 | Emergency charges |
shield | 0 | 0 to 140 | One-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.
| Weapon | Cost | Requires | Stat changes | What it feels like |
|---|---|---|---|---|
| Service Repeater | free | starter | none | One character, one point of damage, clean or not. The honest baseline |
| Empty Hardpoint | free | (second mount only) | none | Nothing fitted. Lighter, quieter, and it scores the most |
| Longbarrel Cannon | 950 | clean +0.4, foul -0.4, pierce +0.25 | The first step toward precision | |
| Scatter Array | 950 | clean -0.05, foul +0.15, splash +0.35, pierce -0.2 | Barely notices a mistake, never hits hard | |
| Arc Welder | 1,700 | level 9 | clean +0.05, foul +0.05, heat decay +9, fire rate -0.1 | Steady and cool running. Still firing when the others jam |
| Flak Battery | 1,900 | level 10 | clean -0.1, foul +0.2, splash +0.6, pierce -0.35 | Wide, forgiving, blunt. Hopeless against armour |
| Lance | 2,000 | level 12 | clean +0.55, foul -0.55, pierce +0.35, fire rate +0.1 | A clean word kills two thirds of the way in |
| Chain Driver | 2,200 | level 15 | clean -0.2, foul -0.1, comboDmg +0.009 | Weak alone. Lives on unbroken streaks |
| Railgun | 3,200 | level 26 | clean +0.9, foul -0.75, pierce +0.5, fire rate +0.25, score x1.08 | The purist option, and the highest scoring weapon |
| Bulwark Repeater | 5 cores | level 28 | clean +0.05, foul +0.3, splash +0.5, hull +20, score x0.95 | Forgives everything. The endurance build, finished |
| Nova Lance | 6 cores | level 32 | clean +0.75, foul -0.6, pierce +0.6, splash +0.2 | A Lance that also throws sparks. The accuracy build, finished |
What those numbers actually do. With no attachment and no upgrades:
| Weapon | Clean | Fumbled | Splash | Pierce | Axis | Power | Characters to kill a clean 7-letter word | Fumbled |
|---|---|---|---|---|---|---|---|---|
| Service Repeater | 1.00 | 1.00 | 0.00 | 1.00 | Balanced | 0 | 7 | 7 |
| Longbarrel Cannon | 1.40 | 0.60 | 0.00 | 1.25 | Precision | 3 | 5 | 12 |
| Scatter Array | 0.95 | 1.15 | 0.35 | 0.80 | Balanced | 3 | 8 | 7 |
| Arc Welder | 1.05 | 1.05 | 0.00 | 1.00 | Balanced | 8 | 7 | 7 |
| Flak Battery | 0.90 | 1.20 | 0.60 | 0.65 | Forgiving | 4 | 8 | 6 |
| Lance | 1.55 | 0.45 | 0.00 | 1.35 | Precision | 3 | 5 | 16 |
| Chain Driver | 0.80 | 0.90 | 0.00 | 1.00 | Balanced | 0 | 9 | 8 |
| Railgun | 1.90 | 0.25 | 0.00 | 1.50 | Precision | 6 | 4 | 28 |
| Bulwark Repeater | 1.05 | 1.30 | 0.50 | 1.00 | Balanced | 15 | 7 | 6 |
| Nova Lance | 1.75 | 0.40 | 0.20 | 1.60 | Precision | 11 | 4 | 18 |
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).
| Attachment | Cost | Requires | Stat changes |
|---|---|---|---|
| Empty Socket | free | starter | none |
| Focusing Lens | 700 | clean +0.2, foul -0.15 | |
| Recoil Damper | 700 | foul +0.28, clean -0.1 | |
| Splitter Prism | 1,400 | level 11 | splash +0.3, clean -0.1 |
| Overclock Coil | 1,500 | level 13 | fire rate -0.25, heat capacity -18 |
| Match Actuator | 1,600 | level 14 | clean +0.35, foul -0.35 |
| Ballast Weight | 1,600 | level 14 | foul +0.4, score x0.96 |
| Armour Piercer | 1,800 | level 17 | pierce +0.4, splash -0.15 |
| Gyro Mount | 1,900 | level 16 | assists +1, clean -0.1 |
| Tracer Feed | 2,000 | level 19 | comboDmg +0.005 |
| Resonator | 4 cores | level 30 | clean +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#
| Hull | Cost | Requires | Stat changes |
|---|---|---|---|
| Standard Frame | free | starter | none |
| Reinforced Hull | 700 | hull +25 | |
| Bulwark | 1,500 | level 8 | hull +45, damage taken x0.9, score x0.97 |
| Glasswing | 2,000 | level 14 | hull -25, score x1.14, salvage x1.1 |
| Mender Frame | 2,200 | level 15 | hull +30, regen 9 per wave |
| Aegis Plating | 2,400 | level 18 | hull +55, damage taken x0.85, shield 30, salvage x0.94 |
| Titan Frame | 6 cores | level 30 | hull +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.
| Targeting | Cost | Requires | Stat changes |
|---|---|---|---|
| Open Reticle | free | starter | none. The highest scoring option in the game |
| Assist Module | 800 | assists +2, score x0.97 | |
| Combo Governor | 1,600 | level 10 | combo kept 50%, assists +1, score x0.95 |
| Predictive Lock | 2,300 | level 20 | assists +3, reveal +1.2 s, score x0.93 |
| Duelist's Reticle | 2,600 | level 24 | score x1.18, heat capacity -20 |
| Oracle Sight | 5 cores | level 28 | combo 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#
| Coolant | Cost | Requires | Stat changes |
|---|---|---|---|
| Stock Vent | free | starter | none |
| Twin Radiator | 650 | heat decay +10 | |
| Cryo Loop | 1,500 | level 9 | heat capacity +45, decay +14 |
| Run-Hot Manifold | 1,800 | level 13 | capacity -25, decay -4, score x1.15, salvage x1.08 |
| Zero-Point Sink | 2,600 | level 22 | capacity +80, decay +26, score x0.96 |
| Reactor | Cost | Requires | Stat changes |
|---|---|---|---|
| Standard Cell | free | starter | none |
| Field Dampener | 1,200 | field speed x0.92, salvage x0.96 | |
| Overcharged Cell | 1,400 | level 7 | score x1.12, heat capacity -15 |
| Salvage Magnet | 1,900 | level 12 | salvage x1.25, heat decay -3 |
| Flux Capacitor | 2,800 | level 24 | field speed x0.88, score x1.1, salvage x1.1 |
| Singularity Core | 7 cores | level 34 | field speed x0.8, score x1.2, salvage x1.2, hull -20 |
| Module | Cost | Requires | Stat changes |
|---|---|---|---|
| Empty Mount | free | starter | none |
| Chaff Launcher | 900 | 2 charges | |
| Emergency Shield | 1,300 | shield 45 | |
| Time Dilator | 2,100 | level 16 | field speed x0.9, reveal +0.8 s, score x0.95 |
| Charge Battery | 2,500 | level 21 | 4 charges, hull +15 |
| Anchor Protocol | 4 cores | level 26 | combo kept 60%, heat decay +8, score x0.94 |
| Prospector's Rig | 5 cores | level 30 | salvage 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
| Item | Base price | +1 | +2 | +3 | +4 | +5 | Total chips | Total cores |
|---|---|---|---|---|---|---|---|---|
| Assist Module | 800 | 360 | 640 | 920 | 1,200 | 1,480 | 4,600 | 2 |
| Lance | 2,000 | 900 | 1,600 | 2,300 | 3,000 | 3,700 | 11,500 | 2 |
| Titan Frame | 2,520 (6 cores) | 1,134 | 2,016 | 2,898 | 3,780 | 4,662 | 14,490 | 2 |
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.
| Stat | Weight per point above base |
|---|---|
maxHp | 0.30 |
dmgMult | -34 |
regen | 1.4 |
heatCap | 0.10 |
heatDecay | 0.55 |
comboKeep | 26 |
assists | 5.5 |
reveal | 6 |
fieldSlow | -46 |
scoreMult | 16 |
salvageMult | 12 |
charges | 7 |
shield | 0.22 |
dmgClean | 15 |
dmgFoul | 13 |
splash | 10 |
pierce | 9 |
comboDmg | 220 |
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:
| Cost | Value |
|---|---|
| The swap drops your lock | Immediately |
| Spin-up | SWAP_MS 600 ms, during which damage ramps from SWAP_DAMAGE 0.5 back to 1 |
| Fitting a second hardpoint at all | score 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.
| Gambit | Multiplier | What it removes |
|---|---|---|
| No Shield | 1.15x | Start with no absorb, whatever your hull says |
| Empty Racks | 1.12x | No emergency charges |
| Hot Start | 1.18x | Every wave begins at half heat |
| Iron Discipline | 1.20x | No assists and no combo protection, whatever your targeting says |
| Paper Hull | 1.35x | Half the base health you would otherwise have |
| Sudden Death | 1.60x | One 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.
| Damage | 2.25 clean, 0.20 fumbled (the floor). Four characters kill a clean seven-letter word |
| Survivability | 75 hull, no shield, no charges, no assists, heat capacity 60 |
| Earning | Score x1.45, the cap. Salvage x1.19 |
| Power rating | 5 |
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.
| Damage | 0.95 clean, 1.58 fumbled. A slip costs almost nothing |
| Survivability | 225 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 |
| Earning | Score x0.80, salvage x1.10 |
| Power rating | 180 |
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.
| Primary | 1.55 clean, 0.45 fumbled, pierce 1.75 |
| Secondary | forgiving and wide, one Tab away |
| Survivability | 130 hull, 9 regen per wave, 1 assist, half the combo kept |
| Earning | Score x0.89 (0.95 for the Governor, times 0.94 for the second mount) |
| Power rating | 44 |
The Prospector. Scatter Array, Recoil Damper, Aegis Plating, Assist Module, Cryo Loop, Salvage Magnet, Prospector's Rig, flown in the Rift.
| Damage | 0.85 clean, 1.43 fumbled, 0.35 splash |
| Survivability | 140 hull, damage taken x0.85, 30 shield, 2 assists, 2 charges |
| Earning | Salvage x1.53, which the Rift then multiplies by 2.2. Score x0.97 |
| Power rating | 70 |
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#
| Currency | Where it comes from | What it buys |
|---|---|---|
| Chips | Time and consistency: every run, every contract, every duplicate | Most of both catalogues, and every upgrade |
| Cores | ONLY bosses and mini-bosses. Nothing else in the game produces one | The 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.
| Rarity | Colour | Derived from | Duplicate value | Base drop weight |
|---|---|---|---|---|
| Standard | #A4B2D2 | free, no requirement | 20% of cost | never dropped |
| Polished | #38DFD2 | anything cheaper than the bands below | 25% | 58 |
| Rare | #8B7BFF | level 10+, or 1,000+ chips | 30% | 30 |
| Elite | #F2B33D | level 22+, or 2,000+ chips, or any badge requirement | 35% | 10 |
| Legendary | #FF5D8F | level 40+, or 3,500+ chips, or a badge plus 2,000 chips | 40% | 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.
| Source | Cosmetic rolls | Chips | Cores | Floor | Rarity weights (polished / rare / elite / legendary) |
|---|---|---|---|---|---|
| Event cleared | 0 | 10 to 26 | 0 | ||
| Cargo Run cleared | 1 | 60 to 140 | 0 | 78 / 20 / 2 / 0 | |
| Mini-boss killed | 1 | 45 to 90 | 0 to 1 | 62 / 28 / 9 / 1 | |
| Boss killed | 2 | 140 to 280 | 1 to 3 | 40 / 36 / 19 / 5 | |
| Boss killed for the FIRST time | 3 | 220 to 400 | 2 to 4 | rare | 30 / 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#
- 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.
- 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. - 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.
| Vendor | Currency | Stock | Pricing |
|---|---|---|---|
| The Quartermaster | chips | Everything, always | List price |
| The Salvager | chips | Five items, rotating at local midnight | 25% off |
| The Archivist | cores | Prestige stock only | Cores, 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)
| Ship | Cost | Requires | Rarity |
|---|---|---|---|
| Standard Issue | free | Standard | |
| Ember | 900 | Polished | |
| Jade Runner | 1,200 | Rare | |
| Nightshade | 1,500 | Rare | |
| Drifter | 1,800 | Rare | |
| Crimson Lance | 2,000 | badge: a 100 combo | Legendary |
| Gilded Arrow | 2,200 | level 15 | Elite |
| Bulwark | 2,600 | level 25 | Elite |
| Void Cutter | 3,200 | badge: wave 20 | Legendary |
| Solar Flare | free | badge: 90 WPM | Elite |
| Phantom | free | Starfall rank 10 | Polished |
| Warden | free | level 40 | Legendary |
Trails (10)
| Trail | Cost | Requires | Rarity |
|---|---|---|---|
| Vapour | free | Standard | |
| Running Dark | free | Standard | |
| Ion Wake | 700 | Polished | |
| Ember Wake | 900 | Polished | |
| Jadewash | 1,100 | Rare | |
| Goldstream | 1,400 | level 12 | Rare |
| Aurora | 1,600 | level 20 | Rare |
| Ghostlight | 2,400 | badge: ten flawless runs | Legendary |
| Solar Wind | free | badge: three bosses in one run | Elite |
| Nova | free | Starfall rank 15 | Polished |
Keycaps (10)
| Keycap | Cost | Requires | Rarity |
|---|---|---|---|
| Slate | free | Standard | |
| Cream | 600 | Polished | |
| Carbon | 800 | Polished | |
| Mint | 1,000 | Rare | |
| Amber | 1,200 | level 10 | Rare |
| Rose | 1,200 | Rare | |
| Dusk | 1,500 | level 18 | Rare |
| Beacon | free | badge: 70 WPM | Elite |
| Ghost | free | badge: no weak keys | Elite |
| Champion | free | level 50 | Legendary |
Name plates (10)
| Plate | Cost | Requires | Rarity |
|---|---|---|---|
| Standard Plate | free | Standard | |
| Steel | 700 | Polished | |
| Beacon Plate | 1,000 | Rare | |
| Amber Plate | 1,300 | level 14 | Rare |
| Rose Plate | 1,300 | Rare | |
| Dusk Plate | 1,600 | level 22 | Elite |
| Void Plate | 2,000 | badge: wave 30 | Legendary |
| Jade Plate | free | badge: 30 curriculum stars | Elite |
| Duellist Plate | free | badge: ten Versus wins | Elite |
| Gold Plate | free | level 60 | Legendary |
Titles (14)
| Title | Requires |
|---|---|
| No title | free |
| Cadet | free |
| Quick Fingers | badge: 50 WPM |
| Surgeon | badge: 99% over 600 characters |
| Regular | badge: seven day streak |
| Wavebreaker | badge: wave 20 |
| Boss Killer | badge: six bosses in one run |
| Scholar | badge: every curriculum node cleared |
| Night Owl | badge: a run between two and five in the morning |
| Centurion | badge: 110 WPM |
| Duellist | badge: fifty Versus wins |
| Marathoner | badge: thirty day streak |
| Collector | badge: twenty thousand chips earned |
| Legend | level 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.
| Stage | Group | Keys added | The idea |
|---|---|---|---|
| Home Keys | Index Anchors | f j | The bumps. Find them without looking and everything else has an address |
| Home Keys | Middle Fingers | d k | One step in from the anchors |
| Home Keys | Ring Fingers | s l | The weakest fingers on the strongest row |
| Home Keys | Little Fingers | a ; | The outside edge |
| Home Keys | The Stretch | g h | The index fingers reach inward. The hand does not move |
| Home Keys | The Whole Home Row | asdfghjkl; | All ten mixed. The first real checkpoint |
| The Top Row | r and u | r u | Up, and back down to the bumps |
| The Top Row | e and i | e i | Two of the most common letters in English |
| The Top Row | w and o | w o | Ring fingers up |
| The Top Row | q and p | q p | The far corners. Everybody is slow here |
| The Top Row | t and y | t y | The inner reach |
| The Top Row | Top Row and Home | qwertyuiop | Two rows mixed. Most short English words are now reachable |
| The Bottom Row | v b n m | v b n m | The index fingers go down |
| The Bottom Row | c and comma | c , | Middle fingers down |
| The Bottom Row | x and full stop | x . | Ring fingers down |
| The Bottom Row | z and slash | z / | The last two corners |
| The Bottom Row | Every Letter | a to z | The whole alphabet in real words. The second checkpoint |
| The Whole Board | Capitals | A to Z | Shift with the opposite hand, never the same one |
| The Whole Board | Punctuation | , . ' " ; : ! ? - | What turns typed words into typed sentences |
| The Whole Board | The Number Row | 1 to 0 | The row nobody practises, and the one that slows real work most |
| The Whole Board | Symbols | !@#$%^&*()[]{}<>/=+_~ plus backslash, vertical bar and backtick | The 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:
| Bar | Value | Why |
|---|---|---|
keySamples | 20 presses | Before the model is allowed an opinion about a key at all |
keyAcc | 90% | EVERY key in the group must clear this. The strict half |
groupAcc | 95% | One completed drill must have reached this |
reps | 1 | At 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.
| Group | Node | Material | Characters | Signpost pace |
|---|---|---|---|---|
| Foundations | Home Row Anchors | home | asdfjkl; | 18 |
| Foundations | The Home Row | home | asdfghjkl; | 22 |
| Foundations | Reaching Up | homeTop | top row plus home | 25 |
| Foundations | Reaching Down | letters | bottom row plus home | 28 |
| Foundations | The Whole Alphabet | letters | a to z | 32 |
| The whole keyboard | Common Words | common | 36 | |
| The whole keyboard | Capitals and Shift | capitals | A to Z | 38 |
| The whole keyboard | Everyday Punctuation | punct | ,.'";:!?- | 40 |
| The whole keyboard | The Number Row | numbers | 1234567890 | 42 |
| The whole keyboard | Symbols and Brackets | symbols | shifted row, brackets, slashes | 45 |
| Real text | Longer Words | commonLong | 48 | |
| Real text | Sentences | prose | 52 | |
| Real text | Paragraphs | prose | 56 | |
| Real text | Code | code | (){}[];=<>_. | 55 |
| Precision | Clean Sweep | common | 45 | |
| Precision | Your Trouble Keys | generated live from your model | 40 | |
| Precision | Awkward Pairs | generated from your worst bigrams | 44 | |
| Speed | Sprint Burst | common | fifteen seconds flat out | 70 |
| Speed | Two Minute Cruise | prose | 74 | |
| Speed | The Long Haul | prose | five minutes without a fade | 78 |
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#
- 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. - 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.
- 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#
| Mode | Clock | Material | Ranked |
|---|---|---|---|
| 60 seconds | 60,000 ms | Mixed everyday words | Yes |
| 15 seconds | 15,000 ms | Mixed everyday words | Yes |
| Quote | untimed | One of 20 real passages, punctuation and capitals attached | No |
| Weak keys | 45,000 ms | 46 chunks built from the keys the model says are letting you down | No |
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.
| Format | Length | Built from the seed | Scored on |
|---|---|---|---|
| Starfall | 90 s | The whole spawn schedule up front: 4 + min(9, wave) threats per 11 second wave, each with its word, entry position and descent rate | Points |
| Sprint | 340 characters, hard stop at 210 s | The same passage | Net 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#
| Constant | Value |
|---|---|
| Room code | 5 characters from ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (no I, O, 0 or 1) |
| Seats per room | 6 |
| Countdown | 5,000 ms |
| Starting hull (starfall format) | 100 |
| Wave length (starfall format) | 11,000 ms |
| Threat damage | 7, or 13 for a long word |
| Seat heartbeat | 15,000 ms |
| A seat is not counted after | 46,000 ms of silence |
| Host considered gone after | 90,000 ms |
| Progress pushed | Every 500 ms, throttled |
| Chat | Last 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:
| Field | Meaning |
|---|---|
played | Races finished |
won | Races won, which requires at least one opponent |
points | Ranking 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 |
banked | The 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:
- 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. - A dotted
updateDocCREATES the map it writes into, so every seat write goes throughvsTouchSeat, 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. - 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 onvisibilitychangeandonline, because backoff measured against a sleeping clock is the wrong shape. - A result is banked exactly once per account, guarded by
state.mp.bankedand 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 contract | Goal | Chips | Cores |
|---|---|---|---|
| Keep Typing | 1,200 correct characters | 120 | |
| Long Shift | 2,500 correct characters | 220 | |
| Three Sorties | Finish 3 runs of anything | 140 | |
| Hold the Line | Reach wave 10 in Starfall | 180 | |
| Deep Patrol | Reach wave 15 in Starfall | 260 | 1 |
| Bounty | Bring down a boss | 200 | 1 |
| Clearing Work | Kill 2 mini-bosses | 170 | |
| Steady Hands | Finish a run at 97% accuracy or better | 200 | |
| Smooth Is Fast | Finish 3 runs at 97% or better | 300 | 1 |
| Unbroken | Reach a combo of 60 | 190 | |
| In the Zone | Spend 45 seconds in the Zone | 240 | |
| Academy Business | Certify an Academy key group | 220 | |
| Range Time | Finish 2 Academy or Training drills | 150 | |
| Storm Chaser | Clear 3 field events | 180 | |
| Time Trial | Finish a Flight Check sprint | 130 |
| Weekly contract | Goal | Chips | Cores |
|---|---|---|---|
| The Long Week | 15,000 correct characters | 900 | 2 |
| Boss Hunter | Bring down 5 bosses | 1,100 | 3 |
| Into the Deep | Reach wave 25 in a single run | 1,000 | 3 |
| Academy Term | Certify 4 key groups | 900 | 2 |
| Precision Week | 10 runs at 97% or better | 1,000 | 2 |
| Flow State | 5 minutes in the Zone | 1,200 | 3 |
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.
| Rule | Value |
|---|---|
| Advances | Once per local day, on any completed run |
| Insurance earned | One token every 5 days kept, capped at 3 |
| Insurance spent | Automatically, on the first day you miss |
| What insurance does | Preserves the streak at its current length rather than growing it |
| Daily bonus | min(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
| Badge | XP | Requirement |
|---|---|---|
| First Flight | 100 | Finish your first run |
| Ten Sorties | 150 | Finish ten runs |
| Fifty Sorties | 300 | Finish fifty runs |
| Hundred Sorties | 500 | Finish a hundred runs |
| Five Hundred Sorties | 1,000 | Finish five hundred runs |
| Ten Thousand Keys | 200 | Press ten thousand keys |
| A Hundred Thousand Keys | 450 | Press a hundred thousand keys |
| A Million Keystrokes | 1,200 | Press a million keys |
Levels
| Badge | XP | Requirement |
|---|---|---|
| Recruit | 100 | Level 5 |
| Ensign | 150 | Level 10 |
| Lieutenant | 250 | Level 20 |
| Commander | 400 | Level 30 |
| Ace | 700 | Level 50 |
| Vanguard | 900 | Level 75 |
| Grandmaster | 1,200 | Level 100 |
Speed
| Badge | XP | Requirement |
|---|---|---|
| Thirty Club | 120 | A run at 30 WPM or better |
| Fifty Club | 200 | 50 WPM |
| Seventy Club | 320 | 70 WPM |
| Ninety Club | 500 | 90 WPM |
| Hundred and Ten | 750 | 110 WPM |
| One Thirty | 1,100 | 130 WPM. Professional pace |
Accuracy
| Badge | XP | Requirement |
|---|---|---|
| Not One Miss | 250 | A run of real length with no mistyped character |
| Ten Times Flawless | 600 | Ten flawless runs banked |
| Surgical | 400 | 99% accuracy over six hundred characters |
| Perfect Wave | 200 | Clear a whole wave without one mistyped key |
| Five Perfect Waves | 450 | Five such waves in one run |
| Steady Hand | 300 | 97% accuracy for a full minute of sprint |
The arcade
| Badge | XP | Requirement |
|---|---|---|
| Wave Five | 120 | Reach wave 5 |
| Wave Ten | 220 | Reach wave 10 |
| Wave Twenty | 450 | Reach wave 20 |
| Wave Thirty | 800 | Reach wave 30. The words stop being words |
| Boss Down | 200 | Your first boss |
| Three Down | 350 | Three bosses in one run |
| Fleet Breaker | 700 | Six bosses in one run |
| Chain of 25 | 120 | A 25 character combo |
| Chain of 50 | 250 | A 50 character combo |
| Chain of 100 | 500 | A 100 character combo |
| Chain of 200 | 900 | A 200 character combo |
| On Fumes | 400 | Finish a run with a tenth of your base left |
| Not a Scratch | 500 | Reach wave 10 without losing any hull |
| Perfect Storm | 900 | Wave 10 with a hundred combo and not one mistyped key |
Learning
| Badge | XP | Requirement |
|---|---|---|
| First Clear | 150 | Clear your first curriculum node |
| Ten Stars | 250 | Ten curriculum stars |
| Thirty Stars | 500 | Thirty stars |
| Full Sweep | 800 | Clear every node at least once |
| Perfect Curriculum | 1,200 | Three stars on every node |
| No Weak Links | 700 | Every key you type often at 95% or better |
| Old Enemy | 450 | Bring a key you used to miss constantly back to 97% |
| Flight Checked | 100 | Take the Flight Check and get placed |
Everything else
| Badge | XP | Requirement |
|---|---|---|
| Quartermaster | 200 | Earn a thousand chips |
| Outfitter | 400 | Earn five thousand chips |
| Collector | 800 | Earn twenty thousand chips |
| Contender | 200 | Play ten Versus games |
| First Blood | 200 | Win your first Versus game |
| Ten Wins | 450 | Win ten Versus games |
| Fifty Wins | 900 | Win fifty Versus games |
| Three Day Streak | 200 | Play three days in a row |
| Seven Day Streak | 400 | Seven days in a row |
| Thirty Day Streak | 1,000 | Thirty days in a row |
| Full Roster | 500 | Finish a run of every game in the arcade |
| Night Owl | 300 | Finish a run between two and five in the morning |
| Second Wind | 400 | Come 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.
| Board | Game | Scope | Ranked on |
|---|---|---|---|
| Sprint, 60 seconds | sprint | 60 | net WPM |
| Sprint, 15 seconds | sprint | 15 | net WPM |
| Starfall, Cadet | starfall | cadet | points |
| Starfall, Pilot | starfall | pilot | points |
| Starfall, Ace | starfall | ace | points |
| Starfall, Nightmare | starfall | nightmare | points |
| Starfall, deepest wave | starfall | wave | wave reached, any difficulty |
| Account level | account | level | lifetime XP |
| Versus wins | versus | wins | races 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.
| Site | Firestore collection |
|---|---|
| Big World Quiz | users/<uid> |
| Big Piano Quiz | pianoUsers/<uid> |
| Big Typing Quiz | typingUsers/<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.
| Branch | Contents |
|---|---|
profile | Placement record: placed, placeOffered, placeWpm, placeAcc, placeLevel |
xp | total, level, chips, cores, earnedChips |
gear | owned (id to level), equipped (slot to id), gambits |
loot | opened, pity, best, byRarity |
meta | Contracts, streak, ascensions, lastRunAt, zoneMsTotal |
games | Per game: xp, rank, plays, timeMs, best by mode key, badges, bosses |
keys, bigrams | The per-key model, capped at 140 and 400 entries |
curriculum | Per node: stars, plays, bestWpm, bestAcc, baseWpm |
academy | Per group: reps, chars, bestAcc, certified; plus graduated |
skill | runs, chars, ewmaWpm, ewmaAcc, bestWpm, bestAcc |
badges | id to { at, n } |
cosmetics | owned list and equipped per slot |
history | Newest first, capped at 200 runs |
settings | Sound 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) |
lb | Leaderboard opt-in, signature, country |
mp | Versus tally and banked game ids |
Three rules hold this together:
normalizeState()is the only source of shape guarantees. Any path that replacesstatewholesale (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 infreshState().saveState()is the ONE writer. It stampsupdatedAtand pokes the cloud through a hook, so a mutation can never quietly skip the sync.- 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:
| Page | Stylesheet | Script | Depends on the app |
|---|---|---|---|
index.html (front page) | css/home.css | js/home.js | No. It imports nothing from js/, fetches nothing and touches no localStorage |
app.html (the game) | css/app.css | js/app.js (ES modules) | It is the app |
bible.html (this document) | css/bible.css | js/bible.js | No. 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.
| Layer | Modules |
|---|---|
| Pure helpers | util.js (imports nothing) |
| State | state.js |
| Rules | xp.js, words.js, skill.js, academy.js, gear.js, loot.js, meta.js, vendors.js, achievements.js, cosmetics.js |
| Presentation | art.js (the asset registry and its vector fallback), music.js (the four-track score) |
| Services | audio.js, cloud.js, board.js, nav.js |
| The funnel | run.js |
| Games | games/registry.js, games/starfall.js, games/starfall3d.js, games/sprint.js, games/events.js |
| Panels | modes/arcade.js, modes/train.js, modes/progress.js, modes/ranks.js, modes/armory.js |
| Multiplayer | versus.js |
| Shell | app.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():
| Hook | Breaks the cycle |
|---|---|
setSyncHook | so state.js never imports cloud.js |
setIdTokenProvider | so board.js never imports cloud.js |
setAfterAuth, setBusyProbe | so 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.
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.- Only complete runs count.
abandonRun()is the single exception and it takes ONLY the keystrokes. normalizeState()owns the state shape, andsaveState()is the one writer.#kbCatchis persistent and must never be rebuilt or re-parented. A phone raises its keyboard only for a focused input.- Nothing is ever gated.
- The skill model is the only thing that decides word tiers.
- The Academy never scores speed.
- The placement and the Academy head start only ever RAISE.
- Gear must never multiply XP.
- Every strong gear item keeps a downside.
- Cores come from bosses and mini-bosses and nowhere else.
- The events ledger counts characters PER ENCOUNTER.
- Loot rolls only on CLEARED encounters.
- Cosmetics never affect gameplay.
- The seeded PRNG is what lets Versus work without a referee. Introducing a bare
Math.randominto a seeded path silently desynchronises a race. - 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
escapeHtmltoo. _headersnames every origin the page may reach. A new external dependency is two edits, not one.- Sound effects are synthesized, music is streamed, and the line between them is the point. Every effect is made at runtime in
js/audio.json 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. - 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".
- 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.
- 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 withassets/art/deleted. vendor/is pinned, version in the directory name, and an upgrade is its own deliberate commit.- 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:
| Command | What it proves |
|---|---|
node tools/check-modules.mjs | The 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.py | Every 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:
| Deployable | Where it lives | Where it goes |
|---|---|---|
| The static site | this repo root | Cloudflare Pages |
| The backend Worker | the big-world-quiz repo | Deployed 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:
| File | What it does |
|---|---|
_headers | Security headers and the Content-Security-Policy that names every origin the page may reach |
_redirects | The 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 WPM | XP per minute | Minutes from level 1 to 10 | To level 20 | To level 42 (the placement cap) |
|---|---|---|---|---|
| 25 | 12 | 292 | 1,371 | 7,351 |
| 40 | 36 | 97 | 457 | 2,450 |
| 60 | 94 | 37 | 175 | 938 |
| 80 | 184 | 19 | 89 | 479 |
| 100 | 311 | 11 | 53 | 284 |
| 130 | 576 | 6 | 29 | 153 |
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.
| Route | Best available multiplier on earning |
|---|---|
| Gear, score | 1.45x (hard clamp) |
| Gear, salvage | 1.60x (hard clamp) |
| Sector, salvage | 2.20x (the Rift) |
| Gambits, stacked | 3.94x |
| Ascension, per level | 1.06x each |
| Typing faster | 0.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:
| Accuracy | XP 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#
| Term | Meaning |
|---|---|
| Assist | A 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) |
| Band | Which length of transmission a boss phase uses: short, medium or long |
| Chips | The soft currency. Time and consistency, not speed |
| Clean | A target that has not been fumbled during this engagement |
| Combo | Consecutive correct characters |
| Core | The hard currency. Only bosses and mini-bosses produce one |
| Encounter | An 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 |
| Gambit | An opt-in handicap that pays a reward multiplier |
| Hardpoint | A weapon mount. There are two, swapped with Tab |
| Heat | Rises on mistypes. Full heat jams the turret |
| Jam | 1,150 ms during which the turret takes no input at all |
| Ledger | The per-encounter record of characters typed, used to score multipliers honestly |
| Lock | The threat your keystrokes are currently going to |
| Pace factor | The superlinear speed term in the XP formula |
| Pity | The counter that guarantees an Elite drop within six rolls |
| Power | One number summarising a loadout, compared against a sector's recommendation |
| Profile | A vulnerability sheet: how an encounter resists splash, pierce and mistakes |
| Reveal | Seconds of extra legibility bought by gear, drawn as a strip above the field |
| Sector | A difficulty band with its own loot weighting. Never a lock |
| Strict | The mistype rule that drops your lock. Automatic from Ace upward |
| Tier | A word pool, chosen from measured skill |
| Transmission | The prose a boss broadcasts, which is what you type to strip a phase |
| The Zone | The 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
| Constant | Value |
|---|---|
MAX_LEVEL | 100 |
| level cost | round(60 + 40 * level^1.28) |
REF_WPM | 40 |
PACE_EXP | 1.35 |
| pace clamp | 0.35 to 8 |
| accuracy factor | (acc/100)^2 |
PLACE_CAP | 42 |
| per-game rank cost | round(120 + 70 * rank^1.15), cap 50 |
| chips | words * 0.5 + minutes * 8, +10 at 97% accuracy, +25 on a best |
The run funnel, js/run.js
| Constant | Value |
|---|---|
MAX_PLAUSIBLE_WPM | 260 |
MIN_RUN_MS | 2,000 |
| gambit multiplier clamp | 1 to 4 |
| loot multiplier clamp | 0.5 to 6 |
The arcade, js/games/starfall.js
| Constant | Value |
|---|---|
STEP / MAX_FRAME | 1/60 s / 0.25 s |
VIEW_TOP / REVEAL_TOP / BASE_Y | -0.04 / 0.12 / 0.93 |
SPEED_BASE / SPAWN_BASE | 0.026 heights per second / 2,900 ms |
JAM_MS / JAM_RESET / HEAT_PER_MISS | 1,150 ms / 45% / 13 |
BREATHER_MS / COUNTDOWN_MS | 4,200 ms / 2,200 ms |
COMBO_STEP / COMBO_CAP | 10 / 4x |
ZONE_WINDOW | 40 keystrokes |
SCORE_CHAR / SCORE_KILL / SCORE_PER_LETTER / SCORE_CLEAN | 2 / 12 / 6 / 30 |
SCORE_PHASE / SCORE_MINI / SCORE_BOSS / SCORE_WAVE | 400 / 600 / 1,500 / 100 per wave |
SLOW_MS / FOCUS_MS / EMP_MS | 8,000 / 10,000 / 6,000 ms |
MAX_PARTICLES | 260 |
PROGRESS_MS | 250 ms (how often a versus room hears from us) |
Encounters, js/games/events.js
| Constant | Value |
|---|---|
BOSS_EVERY | 5 waves (4 in the Rift) |
CHARS_PER_SHOT | 6 |
| encounter cooldown | 3 waves |
| encounter chance | clamp(0.22 + wave * 0.02, 0.22, 0.55) |
| mini-bosses appear | wave 6 |
| lap scaling | damage +22%, chips +35%, escort +1 per lap |
| band caps | short under 30 WPM, medium under 55 WPM |
Gear, js/gear.js
| Constant | Value |
|---|---|
MAX_UPGRADE / UPGRADE_STEP | +5 / 14% per level |
SWAP_MS / SWAP_DAMAGE | 600 ms / 0.5 |
SECOND_HARDPOINT | score x0.94, heat capacity -10 |
COMBO_DMG_CAP | +60% |
SKILL_POWER_PER_WPM | 1.2 |
Salvage, js/loot.js
| Constant | Value |
|---|---|
PITY_AT | 6 rolls |
| duplicate conversion | 20% to 40% of list price by rarity |
| first kill floor | Rare |
The meta layer, js/meta.js
| Constant | Value |
|---|---|
INSURANCE_EVERY / INSURANCE_CAP | 5 days / 3 tokens |
COMEBACK_AFTER_DAYS | 5 |
ZONE.enterCombo / enterAcc / graceMisses | 20 / 96% / 1 |
ZONE.scoreMult / xpPerSec | 1.25x / 3.2 |
ASCEND_AT / ASCEND_BONUS | level 100 / +6% each |
The Academy, js/academy.js
| Constant | Value |
|---|---|
CERT.keySamples / keyAcc / groupAcc / reps | 20 / 90% / 95% / 1 |
| Groups / stages | 21 / 4 |
The skill model, js/skill.js
| Constant | Value |
|---|---|
MIN_KEY_MS / MAX_KEY_MS | 40 / 3,000 ms |
KEY_INERTIA / KEY_ALPHA_MIN | 0.25 / 0.1 |
KEY_KEEP / BIGRAM_KEEP | 140 / 400 |
SKILL_ALPHA_MIN / RUN_FULL_MS | 0.12 / 30,000 ms |
CLEAR_ACC / STAR_ACC_3 | 92% / 96% |
STAR_STEPS | 1.05x, 1.12x, 1.22x of your own baseline |
BASE_UP / BASE_DOWN / MIN_BASELINE | 0.35 / 0.06 / 8 WPM |
MIN_KEY_SAMPLE / OUTGROWN | 12 presses / 1.3x |
Versus, js/versus.js
| Constant | Value |
|---|---|
CODE_LEN / MAX_SEATS | 5 / 6 |
COUNTDOWN_MS / STARFALL_MS | 5,000 / 90,000 ms |
SPRINT_CHARS / SPRINT_CAP_MS | 340 / 210,000 ms |
HB_MS / AWAY_MS / HOST_GONE_MS | 15,000 / 46,000 / 90,000 ms |
WAVE_MS (race) / START_HP | 11,000 ms / 100 |
Presentation, js/art.js, js/music.js and js/state.js
| Constant | Value |
|---|---|
| Music crossfade | 900 ms |
| Music tracks | ops, hangar, combat, boss |
| Music default | on, at volume 0.5 |
settings.render3d default | off. The flat renderer is the site |
| Renderer addresses | /3d and /2d, which redirect to app.html?3d=1 and ?3d=0 |
| Sprite fallback | spriteOk(), with the hand-drawn shape behind every call |
State, js/state.js
| Constant | Value |
|---|---|
STORE_KEY | typingQuiz.v1 |
HISTORY_CAP / KEY_CAP / BIGRAM_CAP | 200 / 140 / 400 |
TYPING_BACKEND_URL | the shared worker, or empty for fully local |