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
Project for fun
Translation pending
GamesTypeScript · CSSGitHub @SemvanOschOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
A small but fully playable Tower Defense game. Create your own adventurer,
collect champions through summons, deploy them to hold back waves of enemies,
and defeat the boss of each stage.
Built with Vite + React + TypeScript and a plain for the board —
no game engine and no runtime libraries beyond React. All sprites, props,
terrain, sound effects and music are drawn or synthesized in code.Read the full README ↗A small but fully playable Tower Defense game. Create your own adventurer, collect champions through summons, deploy them to hold back waves of enemies, and defeat the boss of each stage.
Built with Vite + React + TypeScript and a plain <canvas> for the board —
no game engine and no runtime libraries beyond React. All sprites, props,
terrain, sound effects and music are drawn or synthesized in code.
npm install
npm run dev # start the dev server (http://localhost:5174)
npm run build # type-check + production build
npm run lint # eslint
npm run typecheck # tsc only
npm run champions # dev server opened on the champion stats sheet (#champions)
npm run enemies # dev server opened on the enemy stats sheet (#enemies)
npm run balance # headless balance simulator (tools/balance-sim)
npm run editor # content editor (http://localhost:5175), see "Content editor" below
Strict one-directional layering (ui → application → domain, with engine a
self-contained sibling), so new content is mostly a data edit:
src/
domain/ Pure game data & rules (no framework)
units.ts Champion catalog + in-stage upgrades ← add champions here
enemies.ts Enemy catalog + bosses (BOSS_META) ← balance here
levels.ts Chapters, stages, paths/lanes & waves ← add stages here
endless.ts Endless-mode maps and foe pools
mastery.ts Permanent skill trees
armor.ts The adventurer's armor: slots, rarities, stats, sets
playerChampion.ts, playerSprite.ts, proficiency.ts The custom adventurer
rarity.ts Rarity ladder (Common and Rare are summonable)
targeting.ts Targeting strategies
combat.ts Crit rules
atmosphere.ts Stage moods (lighting, weather, grade)
decor.ts Board themes and props
grid.ts, journal.ts
application/ Use-cases / state
gameState.ts Central GameState model + pure transitions
summon.ts Summon roll (rarity weights)
store.tsx React binding: owns state, persists, exposes actions
infrastructure/
storage.ts The ONLY localStorage access point
engine/ Real-time battle simulation (framework-agnostic)
GameEngine.ts The tick loop: spawns, movement, combat, waves, win/lose
renderer.ts Canvas drawing (reads the engine; no rules)
sprites.ts, figure.ts, anatomy.ts Procedural figures + compositor
terrain.ts, props.ts, lighting.ts, vfx.ts, palette.ts, armorArt.ts
types.ts Runtime entity types
ui/ React screens, components and synthesized audio
App.tsx, screens/, components/
UNITS in domain/units.ts (including its
deployLimit, the per-stage placement cap). Give it a sprite in
engine/sprites.ts or it falls back to its emoji.ENEMIES in domain/enemies.ts; a stage boss is a
BOSS_META entry.domain/levels.ts (path or
lanes, and waves). Give it an optional theme, decor and mood to dress
it — open Settings → Developer → Level Designer to draw paths, stamp
props and preview moods, then paste the exported snippet into the spec.EndlessSpec in domain/endless.ts.available: true in domain/rarity.ts and author
champions of that rarity — the summon roll picks it up automatically.TARGETING_STRATEGIES in
domain/targeting.ts; add it to SELECTABLE_TARGETING to expose it in-stage.MASTERY_TREES entry in
domain/mastery.ts.domain/armor.ts: drop chances in
ARMOR_DROPS, rarity odds / stats per piece / salvage gems in
ARMOR_RARITIES, the rollable stats and their ranges in ARMOR_STATS, and
each set's piece names and 4-piece bonus in ARMOR_SETS (one per chapter via
section). Settings → Developer has an armor forge.All persistence flows through application/store.tsx + infrastructure/storage.ts.
Nothing in the UI touches localStorage or game rules directly.
A separate app in tools/editor/ for authoring levels, waves, enemies, bosses,
champions and skill trees. It is not part of the game: nothing in the game
links to it, and npm run build doesn't bundle it. It reuses the game's domain
catalogs, GameEngine, renderer and sprites, so what you edit is drawn and
simulated by the real code.
npm run editor # dev server at http://localhost:5175 (auto-bumps if taken)
npm run editor:build # type-check + static build into tools/editor/dist
npm run editor:preview # serve that build
npm run editor:check # headless self-check of validation, source export and packs
Workflow. The editor opens on the content shipped in src/domain. Edits go
into a working copy with undo/redo (Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y). Save
(Ctrl+S) stores it as the draft in this browser's localStorage (under
aetheria-editor:; the game's aetheria-td: saves are never read or written),
and Discard goes back to the last save. The top bar shows whether there are
unsaved changes. Unsaved work is also kept in a recovery slot, so a closed tab
or crash offers to restore it on the next launch. If src/domain changes
after you saved a draft, the draft is carried onto the new content one entity
at a time. Your version wins where both changed, and a notice lists those
entities.
DISABLED_LEVEL_IDS) or delete stages.UnitDef field (only shapes that have a sprite),
in-stage upgrade tiers and forks, and derived DPS/max stats from the game's
own helpers. The three hero champions are read-only.Validation. Every change is validated by one module
(tools/editor/src/validation/validate.ts): errors, warnings and notes appear
inline at the field and in the Problems drawer, each saying what is wrong and
how to fix it. Click a problem to jump to it. Errors block content packs and
source export, but a backup can always be taken.
Test play. A level's Test play button runs it on the real
GameEngine with the draft content installed. You can set starting gold,
mastery (none or full), which heroes to bring and the RNG seed, then use
pause, 1–8× speed, step, skip to the end of the wave, restart and extra gold.
It never touches progression or game saves.
Content packs. Content Packs → Export writes a versioned JSON file:
{
"format": "aetheria-content-pack",
"version": 1,
"kind": "content", // or "backup": a draft as-is, errors and all
"scope": "full", // or "changes": only what differs from shipped
"createdAt": "2026-10-17T12:00:00.000Z",
"validation": { "errors": 0, "warnings": 1 },
"content": { "levels": [], "enemies": [], "bosses": [], "units": [], "mastery": {} },
"removed": ["enemy:cas_brute"], // changes only: shipped entities deleted
"order": { "levels": ["level:1"] } // changes only: a collection's order, when it changed
}
A content pack is only written when there are no errors. Import (file, drop
or paste) previews the result first. A full pack replaces the working copy or
is merged in; a changes pack is merged in. Fields the game doesn't read can be
stripped. An import is unsaved until you press Save, and Undo reverts it.
Older pack versions are upgraded on import.
Back into the game (source export). Content Packs → Generate source files
patches src/domain/levels.ts, enemies.ts, units.ts and mastery.ts in
memory. Only the changed entries are rewritten, in the files' own style, so
comments and formatting elsewhere survive and unchanged files come out
byte-identical. Review the per-file diff, then copy or download the files. On
the editor's dev server you can also press Write N files to src/domain.
This is an explicit, confirmed action: it saves your draft first, then the dev
server checks each file still matches the version the editor loaded (if one
changed it writes nothing and asks you to reload), then writes every changed
file or none. Only those four files can be written, and only from npm run editor
(never from a build or preview). Review the result with git diff, then run
npm run typecheck.
Reset. Content Packs → Reset to the shipped content throws the draft away after a confirmation that offers a backup download first.