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 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
A from-scratch, browser-native homage to the classic SimTower (1994): a
vertical metropolis you build floor by floor. Wire it with elevators, attract
tenants, set the rents, keep everyone happy, and climb the star ratings all the
way to a coveted TOWER.
Written in TypeScript on the Excalibur.js
game engine (camera, scene, culling, collision and the render loop), with a
procedural WebAudio soundtrack that changes depending on which part of the
tower you're looking at. No external art assets: every sprite is drawn in code.查看完整 README ↗Play it: verticopolis.com
A from-scratch, browser-native homage to the classic SimTower (1994): a vertical metropolis you build floor by floor. Wire it with elevators, attract tenants, set the rents, keep everyone happy, and climb the star ratings all the way to a coveted TOWER.
Written in TypeScript on the Excalibur.js game engine (camera, scene, culling, collision and the render loop). The simulation has two implementations that are held to the same conformance tests: the TypeScript one and a Rust one compiled to WebAssembly (see Simulation engines). A procedural WebAudio soundtrack changes depending on which part of the tower you're looking at. No external art assets: every sprite is drawn in code.
npm install
npm run dev # open the printed localhost URL
Other scripts:
npm run build # production build to dist/
npm run preview # serve the production build
npm run screenshots # build + headless-capture screenshots into docs/screenshots
Contributing? See CONTRIBUTING.md for the dev workflow:
the quality gates (typecheck, lint, test, build), the two test tiers, and
the enforced coverage floors.
The production build (npm run build) is an installable Progressive Web App.
Open the served build in Chrome, Edge, or Safari and use Install app / Add
to Home Screen to run Verticopolis in its own window, offline.
It's built on vite-plugin-pwa (Workbox
under the hood), so there's no hand-rolled service worker. A few details worth
knowing:
gallery/preview tooling pages are excluded from its scope and
precache.src/public/ and are generated from an in-code SVG (no
external art), matching the game's "every sprite drawn in code" ethos. Regen
with npm run icons.Floor tiles, then place rooms on top of them.
The ground floor and every 15th floor want a Lobby.Elevator or Stairs chain back to the
ground lobby. Unreachable tenants get unhappy and leave.Housekeeping), and shops,
restaurants and cinemas earn from foot traffic.Wedding Hall on floor 100 and
pass the VIP inspection to become a TOWER.| Action | How |
|---|---|
| Pan | Drag with the Inspect tool, middle/right mouse, or hold Space |
| Zoom | Mouse wheel |
| Build a room | Pick it from the left palette, click on a floor |
| Build/paint floors | Pick Floor/Lobby, click-drag |
| Build an elevator/stairs | Pick it, drag vertically to set the span |
| Edit a facility | Inspect tool, click a room or shaft → edit panel |
| Bulldoze | Bulldoze tool, click (or drag) |
| Game speed | Top-right buttons, or number keys 0–3 |
| Undo / Redo | Ctrl+Z / Ctrl+Shift+Z (⌘ on Mac), or the ↩ ↪ top-bar buttons |
localStorage, multiple save slots, plus
tower-file (.vctower) export/import.For players. The game autosaves to your browser's localStorage and
restores that slot on the next launch. You also get 3 named manual slots so
you can keep several towers, and tower-file export/import for backups or
sharing a tower with someone else. Export downloads your tower as a compact,
compressed .vctower file (a fraction of the size of the old JSON
exports), and Import loads one back through a file picker. Saves are managed
from the in-game saves panel; clearing your browser storage erases them.
The game can also import and export the original game's .TDT save files,
so a 1994 tower can be opened here and a Classic tower built here can be written
out as a .TDT (Modern towers are refused, since SimTower can only load Classic
ones). The format is documented in docs/canon/tdt-format.md.
How it works. A save is a snapshot of the headless simulation, not the
renderer. Simulation.serialize() writes the deterministic source of truth into a plain
SerializedGame object: money, star rating, the game clock, the RNG seed,
every unit and transport, the event system (fires, seasonal events) and
excavation history. SaveGame (src/storage/SaveGame.ts) stores that object
in localStorage. On load, Simulation.deserialize() rebuilds the
model and the Excalibur scene is regenerated from it.
Why not Excalibur's
Serializer? Excalibur ships a serializer, but it targets the render layer: Actors, Components, the scene graph. In this project those are derived view state, rebuilt from the simulation whenever the tower changes; they hold none of the authoritative game state. Serializing them would mean two save paths and a format coupled to the renderer, so the save system deliberately serializes the headlessSimulationinstead.
Saves are treated as untrusted input. deserialize() coerces non-finite
numbers, drops units/transports with an unrecognized kind, and clamps car counts
and shaft heights, so a hand-edited, corrupt, or foreign file degrades gracefully
rather than crashing the game. A version field plus a migrateSave() seam
gates loading: a save from a newer build loads best-effort instead of being
discarded, and any future format change has a single place to add an upgrade
step. The serialize → deserialize → serialize round-trip is covered by the
Vitest suite.
How the clone maps to the original's mechanics. Items marked ✅ are implemented and covered by the Vitest suite and/or the captured screenshots.
| Original mechanic | Status |
|---|---|
| Build floors, then rooms on top; ground + sky lobbies | ✅ |
| Lobby floors are transit-only (no rooms) | ✅ |
| ~100 floors up, basements below, continuous numbering (B1 = floor 0) | ✅ |
| Offices (quarterly rent) | ✅ |
| Condos / apartments (one-time sale, ~2×–2.5× cost, owner buy-back on loss) | ✅ |
| Hotel single / double / suite (nightly, need housekeeping) | ✅ |
| Fast food, restaurant, shop (foot-traffic income, business hours) | ✅ |
| Multi-floor cinema, party hall | ✅ |
| Parking, security, medical, housekeeping, recycling | ✅ |
| Whole-floor basement metro that brings visitors & eases the commute | ✅ |
| Cathedral → religion-agnostic Wedding Hall on floor 100 | ✅ |
| Stairs, escalators, standard / service / express elevators | ✅ |
| Adjustable elevator cars + per-floor (express) stop config | ✅ |
| Demand-driven elevator cars (SCAN dispatch toward waiting floors) | ✅ |
| Star ratings 1–5 → TOWER, population thresholds + facility gates | ✅ |
| VIP inspection for the final TOWER rating | ✅ |
| Elevator overcrowding stresses tenants; the unhappy leave | ✅ |
| Rush-hour demand (morning/lunch/evening) vs. quiet nights | ✅ |
| Stressed crowds turn red when transport is overwhelmed | ✅ |
| Construction time with scaffolding; destruction/sell refunds | ✅ |
| Fire that spreads unless security/medical contain it | ✅ |
| Bomb threats (defused by security) on prestige towers | ✅ |
| Buried treasure found while excavating basements | ✅ |
| Living tower: walking people, riding cars, the metro train, day/night | ✅ |
| Save/load, multiple slots, .vctower export/import | ✅ |
| Per-person stress/routing simulation |
Desktop vs. mobile: the desktop layout mirrors the original's dollhouse
cross-section; mobile gets a responsive portrait layout with an icon toolbar and
touch pan/pinch (see docs/screenshots/09-mobile.png).
src/
engine/ # pure simulation, no DOM
types.ts shared types
facilities.ts facility catalog, costs, star thresholds, grid constants
Clock.ts game time (days, weekdays, day phases, quarters)
rng.ts deterministic PRNG (mulberry32)
Tower.ts spatial model: two-layer grid, placement rules, reachability
Simulation.ts economy, population, satisfaction, ratings, events, save
render/ # presentation
excalibur/TowerEngine.ts Excalibur.js scene shell; delegates to friend-modules below
excalibur/towerScene.ts scene construction, graphics baking, sky, lifecycle
excalibur/towerReconcile.ts retained-actor reconciliation (rooms, structure, facade)
excalibur/towerCrowd.ts cars, train, garbage/garage traffic, ambient walkers
excalibur/towerInputCamera.ts pointer input, picking, camera, coords, audio focus
excalibur/towerOverlay.ts 2D overlay/stats painters + decorative event visuals
sprites.ts procedural per-facility drawing (drawn into Excalibur canvases)
pixelSprites.ts dollhouse room interiors + walking/seated people
ui/ # DOM controls: palette, status bar, editor panel, modals, toasts
UI.ts shell class; delegates to friend-modules below
templates/ lit-html template bodies; uiDialogs.ts dialog controllers
uiPanels.ts editor/inspector panels; uiStatus.ts status+log pump; uiPalette.ts palette build
audio/Audio.ts # location-based procedural soundtrack + SFX
# ToneAudioEngine (orchestrator) + toneScenes (data/math) + toneVoices (synthesis)
storage/SaveGame.ts # localStorage + .vctower export/import
main.ts # GameApp: tool semantics, sim tick, glue (input/camera via Excalibur)
gallery.ts # standalone sprite-catalog page (docs/screenshots)
tests/ # Vitest unit tests for the engine
The engine is deliberately DOM-free and deterministic so it can be unit
tested in isolation (npm test). Rendering, the camera, panning, zooming and
pointer input all run on Excalibur.js:
TowerEngine owns the game loop and scene, drawing each facility, transport and
merged structural "run" as an Excalibur actor whose graphic reuses our pixel-art
sprite code. main.ts only supplies tool semantics through the engine's
controller hooks and advances the simulation each frame. It never touches the
camera or raw pointer math directly.
For diagrams of the layers, the frame loop, the engine subsystems, input flow, and persistence, see ARCHITECTURE.md.
The game runs on the Rust engine (engine-rs/), compiled to WebAssembly,
which it drives directly through EnginePort. Every deploy is the wasm-only
build: there is no engine choice in Settings and no TypeScript fallback, and if
the engine cannot start the game stops with a message. The version line in
Settings and Help names the engine that is running.
The old TypeScript engine (src/engine/) is still in the repository, as the
reference the conformance lock and the tests compare the Rust engine against, and
because the interface still reads some of its data. It is not run by the shipped
game and will be deleted in a later release. During the cutover the game moved
onto one synchronous numeric interface, EnginePort; see
ARCHITECTURE.md for
the design and CONTRIBUTING.md for building the engine
package (npm run wasm:build).
See docs/screenshots.md for how these are captured and how to add before/after images to a pull request.
| Day | Night |
|---|---|
A humble ground floor climbs the whole rating ladder to the coveted TOWER.
These frames come from the pinned-container screenshot pipeline (the
milestones scene in scripts/scenes/milestones.ts), the same one that
renders the rest of the gallery. The end-to-end test (e2e/milestones.spec.ts)
independently earns every rung on each run and uploads its own captures as a
CI artifact (to the untracked e2e/output/, never this gallery), so the rungs
shown here are continuously proven against what the game actually does.
| 1★ | 2★ | 3★ |
|---|---|---|
| 4★ | 5★ | TOWER |
|---|---|---|
Two tiers: a fast Vitest unit suite (engine, game logic, UI, sprites, audio,
storage: placement, reachability, economy, star gates, housekeeping, save/load
round-trips) and a Playwright browser end-to-end smoke that proves the game
stays winnable and renders. Run npm test and npm run e2e (after
VC_TOOLING=1 npm run build; the flag publishes the window.game handle the
e2e specs drive, which production builds compile away). The full testing
strategy (the two tiers, the enforced
coverage floors, and what's unit-tested vs. integration-covered) lives in
CONTRIBUTING.md → Testing & coverage.
The source code is MIT (LICENSE); the original art and audio are CC BY 4.0 (ASSETS-LICENSE). To get involved, see CONTRIBUTING.md.
Built fresh as a clean-room clone. None of the original game's code or assets are used.
| ◻︎ abstracted as an aggregate model |