Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A three-lane arcade racer that runs in a browser tab. Nine cars, each with its own
ultimate, race up a road that never stops speeding up — through a city, a desert
and a rainbow strip of deep space — dodging puddles, meteors and tumbleweeds,
grabbing items out of mystery bubbles, and barging each other into the barriers.
No install, no build step, no dependencies. One HTML file, one stylesheet and
fifteen JavaScript files, drawn entirely with hand-written Canvas 2D.
▶ Play it · a game by hiyroscriptREADME 전문 보기 ↗A three-lane arcade racer that runs in a browser tab. Nine cars, each with its own ultimate, race up a road that never stops speeding up — through a city, a desert and a rainbow strip of deep space — dodging puddles, meteors and tumbleweeds, grabbing items out of mystery bubbles, and barging each other into the barriers.
No install, no build step, no dependencies. One HTML file, one stylesheet and fifteen JavaScript files, drawn entirely with hand-written Canvas 2D.
▶ Play it · a game by hiyroscript
Open the page, pick a language the first time, then Start race. Pick a mode, pick a car, and the lights go out three seconds later.
Cars & more on the home screen is the reference: every car's ultimate, every track, every trap, the mystery-bubble drop odds, and what each Condition means. It is the in-game manual and it is built from the same tables the race reads, so it can never drift out of date.
The HUD, clockwise from the top left: the track name and race clock; the distance you have covered and the nine-car standings, with your row picked out; a ladder down the right showing how far ahead or behind each racer is; your ultimate meter and item box bottom right; your own Conditions as a column of coloured icon circles bottom left; three pink, yellow and cyan shield bars immediately above the boost rail at the lower-left; and the boost bar across the foot. Racers off the top or bottom of the screen get an edge badge with their lane and the gap in metres — or, past 500m, a pair of chevrons on the ladder.
| Key | Does |
|---|---|
← → or A D | change lane (and barge whoever is in it) |
↑ or W — hold | boost |
Shift or Space | ultimate |
E | use the item you are holding |
P or Esc | pause / resume |
Space / Enter on a menu | take the obvious next step (and pick a random car on the car sheet) |
| Gesture | Does |
|---|---|
| swipe left / right | change lane — a held drag keeps stepping across |
| swipe up and hold | boost |
| press and hold one finger (0.35s) | ultimate |
| double-tap (within 0.5s) | use the item |
| tap the ultimate square / item box | ultimate / use item |
The boost swipe latches until the finger comes off, so the travel you have already spent does not keep counting against the next lane change. The ultimate needs one finger held — a second finger on the glass suppresses it.
Browsers report a "standard" mapping for PlayStation, Xbox and most third-party pads alike, so one table covers all of them.
| Control | Does |
|---|---|
| left stick ← / → (or d-pad) | change lane |
| right stick held up (or d-pad up) | boost |
| both sticks clicked in (L3 + R3 / LS + RS) | ultimate |
| R2, or Circle (Xbox: RT, or B) | use the item |
| Options (Xbox: Menu) | pause |
| Cross / Options on a panel | resume, or race again |
| Circle on a panel | leave the race |
Everything is edge-triggered off a per-player snapshot of the previous frame, so a held trigger fires once and a held stick walks across the lanes at a readable pace.
Endless — no finish line. Five bots on the road with you and a road that keeps getting faster. Drive as far as you can; leaving the race banks the distance as your personal best.
Race against bots — pick a difficulty, then race the full distance: reach 3.00×, then three track changes, then 900 metres to the flag. Nine cars, every trap, every pickup, and a finishing order at the end.
Local play — two to four people on one screen, one controller each, on a computer. The screen splits into equal columns and the bots fill whatever seats are left, so it is always a nine-car field. Local play is offered on phones but greyed out with the reason, because it needs a keyboard-and-mouse machine with pads attached.
Local play then asks how you want to race:
Backing out of a custom setup and choosing standard restores the defaults, so you never inherit half a custom race by accident.
Every car has the same mechanical ultimate: a 85-second charge, then 15 seconds at 2× its own pace. Difficulty changes when a bot spends the boost, never its strength, duration or charge rate. All nine do something else with those fifteen seconds as well.
| Car | Ultimate effect |
|---|---|
| Dhaval | 15 seconds at 2× pace, five-level light Obscurity and destruction of contacted road hazards |
| Flann | 15 seconds at 2× pace, on fire — see below |
| Neela | 15 seconds at 2× pace, transformed — see below |
| Lolanthe | 15 seconds at 2× pace, Mind Controlling — see below |
| Verdant | 15 seconds at 2× pace, invisible — see below |
| Rhosyn | 15 seconds at 2× pace, in Aero-Glow — see below |
| Saffron | 15 seconds at 2× pace as an airborne metal dragon; meteor interception and a final touchdown impact |
For the whole fifteen seconds, Flann visibly catches fire and turns into a battering ram. It keeps the ordinary speed boost and the ordinary charge clock; what it gains is priority in a collision.
Racers. Flann destroys racers it rear-ends or barges into. A racer hitting Flann receives the ordinary collision response. Two ulting Flanns cancel the special ram; Flann and ulting Verdant still mutually destroy each other regardless of initiator.
Tumbleweed. Smashed apart on contact. No Slow and no meter cost.
Meteor. Neither the falling rock nor the blast can destroy it, and the ultimate carries on.
Puddle. Shield halves are preserved while the ultimate is active. Water is liquid and cannot be rammed apart, so an ulting Flann still gets the spray over its screen and still wears Obscured — Boosted and Obscured at the same time.
None of this is invulnerability. A respawning or finished racer is still out of reach in both directions, so a ram cannot spawn-kill or touch a recorded result, and two ulting Flanns cancel each other out and trade an ordinary shunt. The moment the fifteen seconds are up, Flann is an ordinary car again. No other car gets any of it.
For the whole fifteen seconds, Neela changes shape. It races as a blue craft with a long energy trail instead of as a car, and it keeps the ordinary speed boost and the ordinary charge clock. What it gains is one exchange and a hazard privilege.
The transformation and the exchange white out the view of each human involved for a moment — Neela's own on activation, both racers' on the exchange — and both cars flash white as they change place. In local play that white belongs to the column it happened in: it never touches the seat next door. The white is visual only. The race does not pause, the meter does not stop, the controls still answer, and the car is steered by the person or the bot driving it throughout. There is no homing and no auto-steer: the exchange happens where the cars actually meet.
None of it is invulnerability either. A respawning or finished racer is out of reach in both directions, so an exchange cannot spawn-kill or touch a recorded result; two transformed Neelas cancel each other out and trade an ordinary shunt; and Neela never gains the ability to wreck anybody. The moment the fifteen seconds are up, it is an ordinary car again.
For the whole fifteen seconds, Lolanthe takes the road with it. A single
queen_note.PNG floats above the car, upright and gently rising and falling,
and everything close enough loses its driver.
pion_note.PNG icons orbit every racer it has taken.Lolanthe does not destroy racers by touching them. An ulting Verdant is immune to the whole of it, reveal included — Lolanthe cannot command a target it cannot see.
For the whole fifteen seconds, Verdant disappears — and only that. It is still physically on the road, still in the race order, still carrying its own measured hitbox, and still something you can run into.
Nothing pinned to the car gives it away: the exhaust out of its pipe, its seat ring and flag, its Condition badges and its edge marker all follow the same per-view opacity, so another player's screen shows no outline of where it is. Its dot stays on the ladder, because it is still in the race. Bots do not get to see what a person could not: an ulting Verdant is out of their targeting, their lane reads and their threat picture, so they can blunder into one exactly as you can.
For the whole fifteen seconds, Rhosyn's own driver is shown a different world — Aero-Glow, a black void streaked with luminous pink, with no rivals, no hazards, no bubbles and no scenery in it. Everybody else's road simply has no Rhosyn on it.
The thing to understand about it is what does not happen. There is never a second race. Rhosyn is not teleported away, not frozen, not put on a private coordinate system and not given a distance counter of its own, and the track the race is running is never replaced. The canonical racer goes on being advanced by exactly the same simulation as everybody else: it steers, it covers ground at the same 2× pace, it crosses track seams, it changes biome, it moves up and down the running order and it can cross the finish line. Aero-Glow is a private view over that one racer, plus the fact that the shared road cannot touch it.
max(existing, 2) so a longer protection is never shortened. There is no
second shield and no Rhosyn-only immunity.The orange base car uses two measured exhaust outlets. Its metal dragon uses six, spans two lanes, and rises above racers and hazards with a shadow at its canonical position. Steering, race distance, finish progress and Mind Control continue normally. Ground hazards and pickups are passed over without damage or collection. After the shared fifteen seconds, the normal car drops rapidly and resolves its measured-hull landing impact.
Saffron flies above road-level interactions, while remaining susceptible to Lolanthe’s Mind Control at canonical road distance. Falling meteors striking the dragon are intercepted without a ground explosion. At expiry Saffron transforms back in the air and drops onto its canonical position, wrecking each reachable racer overlapped by its normal-car hull. This never teleports its race distance.
Where they meet, the priority is fixed and in this order:
Lolanthe is nowhere in that list, because Lolanthe wins no contacts. Its forced lane change enters the list from the top as an ordinary barge made by the racer being pushed.
For every car, Slowed and other legitimate debuffs coexist with the speed multiplier, and only Boosted is added to the Conditions on show, with ordinary boost flames and a generic activation burst. Flann adds its body fire on top of those, and only for the ultimate: an ordinary boost or a boost can lights the exhaust and nothing else. Neela is the same the other way round — its blue energy exhaust is the ordinary boost flag every car has, so boosting, a boost can and the ultimate's own speed all light the pipes, and none of them transforms anything. Only the ultimate does that.
Obscured combines independent water and Dhaval timers into one badge. Transformation whiteouts also use this badge as presentation feedback; they are not hostile debuffs and Cleansed does not cancel transformations.
Mind Controlled is Lolanthe’s directly applied Condition. It is a debuff like Slowed and Skidded: temporary invulnerability refuses it and clears it, a wreck and the finish line take it away, and its badge is derived from the same three-second timer the control lock is.
Dhaval is the eighth racer, using v_dhaval.PNG. Its ultimate covers nearby
opponents’ own views in large white, purple, red and green moving, orbiting,
popping light circles. Obscurity starts at level 1. Each accepted road-hazard
contact adds one level, capped at 5; shield absorption still counts. Destroying
a hazard with an ultimate, a near miss, and a Perfect Dodge do not escalate it.
The four-car-length aura refreshes a three-second tail without resetting severity.
A new episode after expiry begins at level 1.
Ulting Rhosyn and Verdant are immune and clear existing Dhaval Obscurity on activation. Ulting Lolanthe instead receives Cleansed: immediately remove all debuffs and refuse new ones for three seconds, refreshed by exposure. Cleansed does not prevent physical collisions, shield loss or wrecks.
Dhaval’s own ultimate destroys contacted weeds, meteors and puddles. Its shared charge, duration and pace are unchanged. Obscured bots have shorter sight and slower reactions. Reduced motion freezes the light layout while preserving its coverage. See measurements, interaction tests and visual QA.
Lanes. Three of them. Changing lane into an occupied one is a barge: if the other car has room it is shoved across and left labouring; if it is already against a barrier, the hit wrecks it. Either way the lane is yours.
Boost. A full bar lasts 5.5 seconds at 1.5× pace and takes 5 seconds to refill. Run it completely dry and it locks out until it is full again. It will not run while you are wrecked.
Rear-ending. Running into the back of the car in front shunts it forward for 0.8 seconds at 1.35× while you are left labouring. Both cars lose the same time to the bump, and the shunt shows on the shunted car as Boosted.
Wrecks. Being destroyed parks you for 3 seconds, then respawns you Invulnerable for 2 more. That is total — every debuff, hazard, trap and attack — and it phases, so you pass through anything that would otherwise meet you.
A car does not make road contact when it has finished, is wrecked, or is Invulnerable from a respawn. An ultimate grants none of these protections — including Flann's, which still has to physically meet what it destroys and is still refused by all three of them.
Finishing is not a Condition and it is not temporary Invulnerability: it is a race lifecycle state. The instant a car crosses the line it is out of play for the rest of that race. Nothing selects it as a target, nothing tracks it, nothing collides with it, and no debuff, item, hazard or seeker can alter the result it has earned — a seeker already locked on gives the mark up and burns out. All it does from there is roll out onto its parking mark.
Three tracks, swapping every 60 seconds. Where two meet, the ground interlocks along a wandering seam rather than butting up against a line, and the driving surface fades over a stretch rather than at a step.
| Track | Looks like | Hazard |
|---|---|---|
| City | Dark asphalt between pale rooftops, water tanks and helipads. White lane dashes, crosswalks and red hydrants along the kerb. | Puddle |
| Desert | Sand-coloured ground and layered rock, cacti and scrub on the shoulders. Sand drifts across a dark road under faded yellow markings. | Tumbleweed |
| Rainbow space | A road of scrolling rainbow bands over near-black, edged with neon cyan rails. Nothing beside it but a drifting starfield. | Meteor |
The road begins at 1.00×, adds 0.10× every 20 seconds, and reaches 3.00× at 6:40. Actual road scrolling follows this pace, including scenery and hazards. The HUD shows the base multiplier to two decimal places.
Every racer starts with three full shield bars: pink → yellow → cyan. Each puddle or tumbleweed hit outside an active ultimate removes half a bar, consuming cyan → yellow → pink, retaining its normal Obscured or Slow effect on a surviving racer. The sixth hit destroys the racer through the existing 3-second wreck lifecycle; it does not take a seventh hit. During the wreck delay the shield stays empty, and respawning fully restores all three bars, along with the existing temporary invulnerability. This applies equally to players, local humans and bots, with each local view showing its owner's bars.
Meteors bypass durability entirely: their existing destruction rules still apply, even at full shield, and they consume no shield halves. Other wreck sources also leave durability untouched until respawn. A hazard destroyed by an applicable ultimate costs no shield; protected racers and successful dodges lose none either. One puddle encounter can only consume one half, however long the racer overlaps it.
A perfect dodge grants +10 percentage points of ultimate charge: change steering with a real hull collision at most 120ms away, then escape safely. Early lane changes, wide or passive misses, hits, and repeated passes do not qualify. The same geometry and reward apply to players and bots. Protected, finished, wrecked, airborne or off-road racers cannot earn this reward.
Each collected Mystery Bubble grants +5 percentage points of ultimate charge, even while its item rewards remain disabled. Both rewards use the shared charge helper: they never extend an active ultimate’s remaining 15-second duration.
| Hazard | Track | What it does | Ultimate cost |
|---|---|---|---|
| Puddle | City | Water over your screen: Obscured for 2.6 seconds | −5% |
| Meteor | Space | A blinking red ring marks the impact. Anything in the blast is destroyed and respawns after 3 seconds | −10% |
| Tumbleweed | Desert | Rolls across from either side; halves your speed for 1.7 seconds | −5% |
A Flann with its ultimate running smashes the tumbleweed and shrugs off the meteor, at no cost to its meter. The puddle still gets it.
The meteor's ring is a spot on the road, not on your screen, and how long the rock has left is measured in seconds — so it lands where it was always going to land no matter what you do to your own speed. Every ultimate leaves the world clock and other racers’ speed unchanged.
The seeker clears hazards it passes through.
Item rewards are switched off at the moment. Bubbles still drift across the road and are still collected, but nobody — you, a bot or a local seat — comes away holding a Boost can, Oily oil or a Seeker. Nothing has been removed: the items below are the items that come back when
MYSTERY_ITEMS_ENABLEDinjs/data.jsgoes back totrue, and everything in this section describes them as they were and will be. The custom-race mystery bubbles switch is a separate thing and still decides whether rows appear at all. Every collected bubble still grants +5 percentage points of ultimate charge.
Three bubbles drift across the road together, a row every 5,400–8,600 road units. Touch one for a random item and take as many of the three as you can reach — each new one replaces what you hold, and a trade flashes the box so a silent swap still reads.
| Item | Rarity | Drop | Leading | What it does |
|---|---|---|---|---|
| Boost can | Common | 66.7% | 70.6% | 2.2 seconds at 1.55× that does not touch your boost meter |
| Oily oil | Rare | 27.8% | 29.4% | Drops a slick behind you for 15 seconds. The first racer to touch it loses all grip for 4 seconds — reversed steering — and takes the slick with them |
| Seeker | Legendary | 5.6% | — | A missile that hunts the leader, destroying whatever it passes through |
The seeker never drops for whoever is leading; out in front, its share goes to the other two. Invulnerability, a wreck and the finish flag stop a seeker hit; an ultimate does not.
A row does not sit there forever. It flashes and goes on whichever comes first: the last stretch before it drops off the bottom, or a 30-second clock that only runs when the road has all but stopped.
Seven of them, and each one is a small filled circle in its own colour with a plain device inside it. The shape carries the meaning as much as the colour does, so a Condition is never told apart by colour alone.
| Condition | Colour | Icon | Meaning | |
|---|---|---|---|---|
| Cleansed | buff | #94F4DD | sparkle and clearing star | Clears every debuff and refuses new ones for three seconds; physical contact still applies |
| Boosted | buff | #FF9A4A | forward chevrons | Anything making you go faster, whatever put it there: the boost meter, a boost can, an ultimate, or a rear-end shunt |
| Invulnerable | buff | #FFD86B | shield | Respawn protection: pass through road contact and refuse every debuff |
| Slowed | debuff | #8A9099 | arrow brought down to a floor | Anything making you go slower, whatever put it there |
| Obscured | debuff | #B07A4A | crossed-out eye | Puddle water or Dhaval’s five-level light obstruction; also used for transformation feedback |
| Skidded | debuff | #0B0B0C | paired skid marks | No grip: left goes right and right goes left |
| Mind Controlled | debuff | #8A4FE0 | musical note | Lolanthe locks driver commands and can force lane changes |
Where they are shown. Every visible car that is not the owner of the view you are looking through wears its Conditions as a compact column of these circles beside it — bots and other people alike, because what decides is the racer's state and never who is holding the controls. Your own car never carries them: yours go in the bottom-left corner of the HUD instead, as the same circles. In split screen this is worked out per column, so in Player 2's window Player 2 has corner badges and everyone else has beside-car ones, and in Player 1's window Player 2 is a rival and gets beside-car badges like anybody else.
Where several are active they stack in a fixed order — Invulnerable, Boosted, Slowed, Obscured, Skidded — so a stack never reshuffles between frames.
Invulnerable clears the debuffs already on you and refuses new ones for as long as it lasts. Rhosyn and Verdant clear Dhaval Obscurity on ultimate activation. Ulting Lolanthe converts Dhaval’s aura into Cleansed; other existing conditions keep their ordinary rules.
A racer that has crossed the line shows no Conditions at all: it is out of the race, not protected within it. The garage's Conditions tab is where the symbols are learned — Buff and Debuff on two sub-tabs, each card carrying the same circle you see on the road.
Endless has no finish line — it runs until you leave.
Race against bots and local play run to a flag:
At 0:00 the pace is 1.00×. Each 20 seconds adds 0.10× until 6:40 / 3.00×,
which arms the three-track closing sequence, followed by the +900m finish stretch.
Elapsed time alone never starts the closing sequence. The HUD keeps the speed
visible alongside T-3, T-2, T-1, then the metres remaining.
When the third track change lands, the flag is planted 900 metres ahead. Cars cross, are given a place, and roll out onto a staircase of marks past the line — first place furthest, each place behind stopping one step earlier, in alternating lanes — so the field parks in the order it finished. Finishers are out of play entirely: nothing can target or touch them.
On one screen the race ends when your car crosses. On four it ends when the last person crosses; bots still on the road finish behind, as they always do.
A difficulty is not a multiplier on anything the car does. Every setting drives the same car at the same pace off the same charge clock. What changes is how well the driver thinks.
| Easy | Medium | Hard | Brutal | |
|---|---|---|---|---|
| Misses trouble in its own lane | 52% | 26% | 10% | 3% |
| Reaction time | 0.70–1.40s | 0.35–0.75s | 0.18–0.42s | 0.07–0.22s |
| Reconsiders the race every | 0.50–0.95s | 0.34–0.68s | 0.22–0.46s | 0.14–0.30s |
| Reads hazards this much further ahead | — | 110 | 250 | 380 |
| Projects other cars forward | — | 0.35s | 0.80s | 1.30s |
| Overall competence | 0.14 | 0.46 | 0.80 | 1.00 |
| Picks targets deliberately | 0.10 | 0.42 | 0.78 | 1.00 |
| Defends its place | 0.06 | 0.36 | 0.72 | 1.00 |
| Values items and ultimates | 0.10 | 0.46 | 0.82 | 1.00 |
| Decision left to chance | 50% | 26% | 11% | 3.5% |
In practice: Easy is slow to spot trouble and happy to let you by. Medium races you fairly and takes a lane when it needs one. Hard blocks, barges and times its ultimates well. Brutal misses nothing, defends every lane, and wrecks you if it can.
On top of difficulty, each car has a temperament — nerve, spite, patience and guard — that is jittered at the start of every race, so five bots on one setting are not the same bot five times, and the Flann you raced last time is not quite this one.
Bots only ever decide. The doing is handed straight back to the same functions your own inputs call, so a bot barging, dropping oil or spending an ultimate is running your mechanic, not a copy written for bots.
Two to four people, one controller each, on one computer. The flow is: player count → controller discovery → standard or custom → difficulty (or the custom sheet) → each player picks a car in turn with their own pad → race.
The sliders icon in the top right of the home screen opens Settings, a panel over the moving road with every preference in it:
| Group | Setting | Does |
|---|---|---|
| General | Language | English or Français, applied to the whole interface as you press it — the panel included, and without a reload |
| Audio | Sound | On or off, the one switch for every sound the game makes |
| Audio | Master volume | 0–100%, straight on to the master gain, live |
| Accessibility | Reduced motion | System / Reduced / Full |
| Accessibility | High contrast | System / On / Off |
| Interface | Show control hints | The keyboard row along the bottom of the home screen |
At the foot is Restore default settings, which asks once and then resets those preferences only — your language and your personal best are kept.
Six values in localStorage, and nothing else is ever written:
| Key | Holds |
|---|---|
seren.lang | en or fr |
seren.sound | 1 or 0 |
seren.volume | 0–100, default 90 |
seren.motion | system, reduced or full |
seren.contrast | system, on or off |
seren.controlHints | 1 or 0 |
seren.best | your furthest distance, in metres |
Existing saves from the previous name are copied from redline.* on startup
when the corresponding seren.* value is absent. Existing Seren values take
priority, and the old keys remain as a backup. All new saves use seren.*. A
saved value that is not one of the ones listed above — an older build, a hand
edit — falls back to its default rather than reaching the game.
If localStorage is unavailable — a private window, blocked site data — the game
falls back to an in-memory store and keeps working for the session.
Language. English and French. The first visit asks before anything else; after that it lives in Settings.
Sound. All generated with the Web Audio API — no audio files. The context is created lazily on your first interaction, so browsers never refuse it for starting outside a user gesture. Everything plays through one master gain, which is the volume when sound is on and silence when it is off.
One system, from the splash to the finish line, built on four things:
Menus use distinct compositions: an angled scrolling road and oversized wordmark on Home, three mode destinations, a numbered difficulty scale, equal-column player previews, live controller bays, a rule workbench, and a car showroom. The showroom uses the actual car renderer; hovering or focusing inspects a car, and choosing it keeps the original direct-pick behavior. Local setup displays the player roster and current configuration. Cars & More uses a reference index with keyboard-operable tabs. Setup occupies the viewport; the game retains its original race layout.
The race HUD is the same system rather than a second one. The corners hold what you consult: the pause button and the track and clock top left, distance and the running order top right. The bottom holds what you spend: the boost meter in its tray, three shield bars just above it on the left, your Conditions above those, the ultimate and item squares on the right. On a wide window that bottom band closes in on the road instead of stretching to the far corners, so it stays in peripheral vision. The ultimate square fills from the foot as it charges, so the reading is a shape before it is a number.
Local play draws the same instruments on the canvas, once per column, from the same constants — see the HUD section of ARCHITECTURE.md.
prefers-reduced-motion: reduce flattens the page's
animations, and Settings can overrule it either way — Reduced stills the
interface on a device that never asked, Full restores the animation for a
player who would rather have it. Things drawn frame-by-frame on the canvas sit
outside CSS, so they ask the same resolved answer — the item-box swap flash, for instance,
becomes a brightness pulse with the box held still. Every state that is
normally carried by movement also has a still form: a charged ultimate stays
red, your row in the standings stays ticked, a Condition badge appears without
animating in.<button>, the focus ring is visible on
every surface, and a screen that is not showing is hidden outright rather than
faded — so nothing invisible sits in the tab order. Setup makes the home
screen behind it inert, and a pause or a result makes the instruments
inert, so tabbing cannot walk out of the thing in front of you.aria-label on
every icon-only control — pause, back, close, settings, and the two squares you
spend.prefers-contrast: more firms up the hairlines, the secondary
ink, the hover and selection washes and the glass — and High contrast in
Settings applies the same treatment on demand, or turns it off. The palette
does not change: black, white, grey and red, worked harder.
forced-colors: active falls back to system colours with real borders.Any current desktop or mobile browser. The game uses Canvas 2D, Pointer Events,
Web Audio, the Gamepad API, matchMedia and localStorage, and degrades
gracefully where newer features are missing — roundRect, ellipse, Path2D
and canvas letterSpacing all have hand-written fallbacks, backdrop-filter has
an opaque one, and a browser without inert simply keeps the older tab order.
Local play needs a device that reports (hover: hover) and (pointer: fine) — a
computer — plus a controller per player.
There is nothing to install and nothing to build. Any static file server will do:
git clone https://github.com/hiyroscript/seren.git
cd seren
python3 -m http.server 8000
# then open http://localhost:8000
Opening index.html straight off the filesystem mostly works, but a server is
better — some browsers restrict localStorage and canvas reads on file://.
To edit: change a file, reload the page. That is the whole loop.
Because there is no build step, nothing normally catches a typo'd selector, a
translation key that does not exist, or a second const with a name another file
already used — the first two do nothing visible until you open the screen that
uses them, and the third kills the page on load. One script finds all of it:
node tools/check.mjs
node tools/menu-check.mjs
node tools/ultimate-check.mjs
node tools/sprite-check.mjs
node tools/hitbox-check.mjs
node tools/dhaval-check.mjs
It is plain Node with no dependencies — there is no package.json and nothing to
install — and it exits non-zero on failure, so it drops into a git hook or a CI
job unchanged. It verifies:
index.html exists, is deferred, and is in the
documented dependency order, with no inline <style> or <script> left behind#id selector in the JavaScript resolves to an element that
exists in index.htmldata-i18n attribute and literal
t("…") call resolves to a string that existsflannCar(),
neelaCar(), lolantheCar(), verdantCar(), rhosynCar(), saffronCar(),
coleCar() and dhavalCar() are the only
places a racer is compared to a car, the predicates built on them are read by
the contact rules, the hazards, the bot mind, the inputs and the renderer,
neelaFormActive() is still demonstrably narrower than neelaUltActive()
(the alternate body, not the fifteen seconds), racerDetectable() is
demonstrably a question about sight rather than about contact, Mind Control is
set rather than added to anywhere in the source, the shared charge clock,
duration and pace are untouched, every one of the five cars' own timings is a
named constant in the tuning layer with the swap guard kept to a single step,
only sprite cars carry a race scale and only within a measured band, and
carHit() takes its hull and its size from the same model the sprite is drawn
fromG.biome, G.next, G.seam or
G.trackT, no file keeps an Aero-Glow metre count, pose, origin or scroll
beside the canonical one, the departure never reaches teleportRacerToPose()
or rebaseWorld(), and reads — the one
car-specific state it is allowed to know about, because it is the one that is
about there being a body at allRun it before you commit. It takes well under a second.
node tools/menu-check.mjs also checks menu state and event wiring using
DOM/Canvas test doubles, including localization, every Settings preference and
what it reaches, setup, simulated controllers, car turns and pause/results.
node tools/ultimate-check.mjs runs the ultimate, Condition, hazard, contact and
Mystery Bubble behaviour for all nine cars as player, bot and local seat, plus
Neela's exchange, Lolanthe's aura and forced lane change and Verdant's
directional defence in every ownership direction, the documented collision
priority for every pair of ulting cars, the world-position regression that says
moving player one leaves the rest of the road exactly where it was, and
Aero-Glow's own invariants — the phase lifecycle, the canonical position and
biome continuing through it, the isolation in both directions against every
contact rule, hazard, pickup and targeting system, the two seconds of
Invulnerable granted at the frame the car is genuinely back, finishing from
inside it, pause, a forced wreck and a restart;
node tools/sprite-check.mjs covers race sizes, all nine sprite cars' sheets
and measured emitters, the ultimate fire, the transformation flash, Verdant's
per-view opacity and Rhosyn's disappearance from every other view in one to four
columns, and both note effects;
node tools/hitbox-check.mjs covers contact geometry, including the switch
between Neela's two measured hulls, Rhosyn's traced hull with the empty V
between its nose prongs, and the invariant that hiding a Verdant or sending a
Rhosyn away moves nothing. Browser visuals
and hardware still need separate checks; see
the redesign QA record,
the ultimate record,
the Flann record,
the Neela record,
the Lolanthe and Verdant record and
the Rhosyn and Aero-Glow record.
index.html the document shell — screens, canvases, SVG icons, script tags
css/app.css the entire stylesheet
v_flann.PNG Flann’s image-backed vehicle, shared by menus and races
v_neela.PNG Neela’s, likewise
vtm_neela.PNG the shape Neela’s ultimate turns it into, races only
v_lolanthe.PNG Lolanthe’s, likewise
v_verdant.PNG Verdant’s, likewise
v_dhaval.PNG Dhaval’s measured eighth-racer sprite
queen_note.PNG the note that floats above an ulting Lolanthe, races only
pion_note.PNG the three that orbit a Mind Controlled racer, races only
js/ the game, in load order (see below)
tools/check.mjs dependency-free validator for the invariants below
docs/ ARCHITECTURE.md, TUNING.md and the screenshots
upd the current brief
.nojekyll tells GitHub Pages to serve the tree verbatim
The fifteen scripts load in a fixed, dependency-safe order with defer, so each
may rely on the ones above it and nothing starts before every declaration exists.
| File | Owns |
|---|---|
core.js | storage with a memory fallback, the small maths/DOM helpers, capability flags |
i18n.js | every string, the current language, and the sweep that writes them into the page |
data.js | cars, difficulties, temperaments, conditions, items, tracks and every tuning constant |
audio.js | the lazily-created Web Audio context, tones, noise, engine |
runtime.js | canvas and context, road and split-view geometry, the game state object, layout/resize |
ui.js | screen switching and focus gating, garage, custom setup, the car board, select-screen art |
settings.js | every player preference, where it is stored, what applies it, and the Settings dialog |
local.js | seats, player colours, pad discovery, the menu pad loops |
ai.js | the bot mind: sense, weigh, act |
mechanics.js | contact, lanes, boost, wrecks, Conditions, ultimates, items, hazards, particles |
race.js | world seeding, the grid, the lifecycle, the finish, the per-frame update and frame loop |
render.js | all Canvas 2D drawing |
hud.js | the DOM HUD, the condition badges and their icons, the per-seat canvas HUD and the constants they share |
input.js | keyboard, pointer and controller, translated into mechanics calls |
main.js | boot: initial paints, event wiring, splash, settle |
docs/ARCHITECTURE.md goes through how it fits together and where to change what. docs/TUNING.md is every dial in one place.
It is a plain static site with relative asset paths, so it works under any base
path. This repository is served by GitHub Pages from main — push, and the
pages-build-deployment workflow republishes it. Any static host works the same
way: copy index.html, the vehicle and effect .PNG files, css/ and js/ and you are done.
G, CARS, startUlt and the
rest are global. Handy for debugging, and it means a determined player can poke
the state. The only persisted value, the personal best, was always one
localStorage.setItem away regardless.A game by hiyroscript. No license file is present, so default copyright applies until the author adds one.
Shield bars remain ordered pink → yellow → cyan, but lose durability cyan →
yellow → pink. shieldBarFill() supplies both HUDs; SHIELD_GRADIENTS supplies
matching borderless, dividerless gradient fills without changing bubble colors.
SHIELD_MAX remains six halves. Respawn restores them after the existing wreck delay.
hitShield() preserves the current half count while ultOn is active. This
neither regenerates shield nor grants universal invulnerability: surviving
Obscured/Slow effects and car-specific solid-hazard behavior remain intact,
and meteors still bypass durability under their existing destruction rules.
Accepted surviving hazard contacts refresh cosmetic shieldHitT to
SHIELD_HIT_TIME (1 second). Frame updates count it down; wreck, respawn, finish
and race reset clear it. shieldStage() selects the current color and half-state.
drawShieldHit() shows one compact bar above the measured car silhouette,
including the airborne render pass, suppresses the current viewport owner and
uses the car's per-view alpha so invisible racers stay hidden. No-contact states
and dodges produce no feedback. This timer has no gameplay effect.
Cole is the seventh racer. Tap the swap button beside the item holder, press Q, or press L1 / LB on a controller to change forms. Switching has a two-second simulation cooldown and works with items and ultimates disabled. It uses the shared transformation flash and preserves position, boost, ultimate time, and the chosen form through a wreck and the finish.
| Form | Base pace | Boost multiplier | Ultimate multiplier |
|---|---|---|---|
| Car | 1.0× | 1.5× | 2.0× |
| Motorcycle | 1.5× | 1.8× | 2.3× |
These multipliers stack with the ordinary pace effects. Cole’s ultimate clears tumbleweeds and meteors in either form; puddles and opponent attacks retain their usual behavior. Bots switch to the motorcycle after their opening reaction. A new race always begins in car form.
A full field has nine racers: local races with two, three, or four humans have seven, six, or five bots. See Cole measurements and QA. node tools/cole-check.mjs runs the focused integration checks; the ultimate suite includes them too.
Aureolin is the ninth racer. Q, L1 / LB, or the vehicle-form button switches between the normal yellow camouflage car and its armed form with a two-second cooldown. Form survives a wreck; a new race starts normal.
While armed, hold Down / S, the right stick or D-pad down, or swipe down and keep the finger down to fire the turret. Each round adds an independently expiring 0.10× slowdown for 1.5 seconds. The yellow capacity bar sits below red boost, drains in 60 shots at 15 rounds/second, and recharges in 3.5 seconds. After overheating, it must reach 100% before another shot. The yellow bar also works with ordinary boost disabled.
The shared fifteen-second ultimate refills capacity, automatically fires the armed turret without drain, doubles newly fired bullet range from eight to sixteen normal Aureolin lengths, and automatically launches two homing rockets every two seconds when an opponent is ahead. Rockets continually seek the closest eligible racer ahead by race progress, last at most five seconds and remove one half-shield each on contact. The ultimate leaves form choice to the driver.
Flann’s ultimate absorbs shots; Verdant’s ultimate is revealed without damage or slowdown; Rhosyn’s ultimate is unreachable. Bullets miss flying Saffron, while rockets can follow it. Mind Control stops new firing; existing projectiles continue. Both Aureolin forms clear solid hazards during the ultimate; puddles still affect the car and projectiles pass over water.
See Aureolin measurements and QA. Run node tools/aureolin-check.mjs; the ultimate suite imports it as well.
noContact()rhosynElsewhere()