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,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Deterministic Frontend Architecture for Complex Interactive Systems
This is a web application built to explore how complex, highly interdependent rulesets can be modeled in a way that keeps the frontend predictable, testable, and resilient under heavy interaction.
While themed as a tabletop RPG assistance tool, the project’s primary focus is frontend architecture: extracting a deterministic domain layer that dramatically reduces UI complexity and state divergence in React applications.檢視完整 README ↗Live Demo: https://fallow-app.vercel.app/
This is a web application built to explore how complex, highly interdependent rulesets can be modeled in a way that keeps the frontend predictable, testable, and resilient under heavy interaction.
While themed as a tabletop RPG assistance tool, the project’s primary focus is frontend architecture: extracting a deterministic domain layer that dramatically reduces UI complexity and state divergence in React applications.
This project is intentionally over-scoped in domain complexity to stress-test frontend architecture decisions.
It explores:
The goal is not to ship a complete game.
The goal is to demonstrate how a complex system can remain coherent, explainable, and maintainable as interaction density increases — a common failure point in large frontend codebases. This project prioritizes clarity under complexity, not framework cleverness.
This project is best evaluated by examining how rule changes propagate through the system. Most architectural decisions are visible in the domain layer and its relationship to the UI, rather than in feature completeness or visual polish.
At a high level, the system is structured as:
Domain (types, rules, selectors, updaters)
↓
React hooks & thin state layers
↓
UI components
React is treated as an integration layer; authoritative game logic is not implemented inside components or hooks.
All authoritative logic lives in a deterministic domain layer that can be:
The most challenging and intentional part of this project was designing a domain layer that is UI-agnostic, yet frontend-driven.
This inversion dramatically reduces:
app/
├── components/ # Thin UI & interaction panels
├── */.json # JSON templates (domain inputs: characters, equipment, etc.)
├── domain/ # Deterministic rules, factories, selectors
├── hooks/ # UI-facing access to domain/state
├── stores/ # Localized Zustand slices (non-authoritative)
├── actions.ts # Server actions (persistence boundaries)
└── page.tsx # Application entry point
⚠️ Note on Zustand Zustand is used sparingly for localized coordination. It is not used as a global derived-state store. All authoritative game logic remains in the domain layer.
The domain layer is authoritative, so most changes start there and flow outward. Data moves in one direction:
JSON templates (app/**/*.json) → Zod schemas (app/domain/types.ts) → factories (app/domain/factories.ts) → rules + lenses/projections + commands (app/domain/**) → stores/hooks → components
When implementing a feature or fix:
Heuristics:
app/domain/character/rules/** (or scene/rules/**); a stat the sheet edits also gets a lens registered in lenses/index.ts; a shape the UI renders is a view getter in character/lenses/** or a projection in scene/projections/** / item/projections/**.app/domain/**/commands/** returning a new aggregate.app/actions.ts) or redis.ts; treat persistence as I/O, not rule evaluation.Non-negotiable constraints:
All character data flows through the Character / CampaignCharacter aggregate.
Domain functions stay synchronous, pure (no I/O, time, or randomness unless explicitly modeled), and stable under recomputation.
Don't add a setter that bypasses the derived-stat invariant floor(0.5 * STR * DM + base) — edit the stored base term, not the derived output.
No component performs arithmetic on a domain number. Components render values and call handlers; if a component needs a + b, the domain owes it a getter. This is the rule that keeps the thesis honest — every rule leak this project has had (an armor tier table in JSX, an STA-regen formula in a header, a STR-mod damage formula duplicated between a panel and a command) began as one small expression inside a component that nobody thought was a "rule".
When the domain doesn't already expose the shape you need, that's the signal to add a view getter, not to compute it in the component. A view getter is a pure (c: Character) => RenderableShape — the same pattern as getStrikeTerms, extended past scalars to rows and tables (getDamageTiers, getWeaponAttackRows). The tell that you're about to leak a rule is a component reading a raw characteristic (STR, STA, TGH) — it's reading it in order to do arithmetic with it.
ESLint rules enforce the boundary mechanically (eslint.config.mjs), so this is a build failure rather than a code-review habit:
app/domain/** may not import from app/stores/**, React, or Zustand — the domain defines its own types and the store imports them, never the reverse.rules/, lenses/ and projections/ may not import commands/ or the bot — the read side never runs a button to find out an answer.app/domain/{character,item}/** may not form an import cycle — size and scale, then items, then the character's state, then AP prices; a file that imports upward mixes two of those and is split.app/components/** may not import from **/domain/*/rules/**, **/domain/*/lenses/**, **/domain/*/projections/** or **/domain/*/commands/** — go through a hook in app/hooks/. Type-only imports are allowed via import type. Inert modules (domain/types, domain/tables, domain/factories, , ) stay importable. is exempt: it's a stress-test harness whose whole purpose is to drive the lens registry and the store directly.Game mechanics exist to anchor abstractions, not to showcase feature breadth.
They were chosen specifically because they create non-linear, cascading effects, which are useful for testing deterministic modeling.
Examples include:
These mechanics ensure that small changes can have wide-reaching effects — exactly the kind of scenario that breaks naïve frontend state management.
git clone https://github.com/gbelow/udgs-runtime.git
cd udgs-runtime
pnpm install
Create .env:
UPSTASH_REDIS_URL=...
UPSTASH_REDIS_TOKEN=...
Run locally:
pnpm dev
Open:
http://localhost:3000
This project prioritizes long-term coherence and change tolerance over short-term feature velocity. The central architectural decision was to model the game as a deterministic domain that fully owns rules, invariants, and data interpretation, with the UI acting purely as a consumer of domain outputs. This allows rule changes to be implemented by modifying the domain layer alone, without requiring coordinated updates across components, stores, or persistence logic.
State in the application is treated as derived by definition: the domain describes how the system behaves, not merely the shape of stored data. As a result, changing data or rules implies updating the domain logic rather than patching downstream consumers. This consolidation was intentional—it trades flexibility at the edges for clarity at the core, and prevents the UI from silently diverging from the rules it is meant to represent.
Global state libraries were deliberately avoided for derived logic. Redux was rejected due to its boilerplate cost and focus on action-based mutation rather than memoized computation. Zustand is used selectively as a coordination layer, primarily to manage memoization boundaries and prevent unnecessary re-renders under heavy interaction. It does not own rules or derived values; those remain in the domain.
For persistence, Redis was chosen over a relational database because the system operates on self-contained aggregates rather than relational queries. Characters and related entities are loaded and saved as whole objects, without joins. This simplifies iteration but introduces the responsibility of handling data evolution explicitly. The system assumes that data shape changes are intentional and versioned, with the domain acting as the authoritative interpreter of stored data rather than relying on implicit schema guarantees.
JSON files are used for domain inputs (characters, equipment, rules tables) both for transparency and for ease of bulk editing. This choice made experimentation and reshaping faster during development and reinforced the separation between data definition and rule evaluation. While not optimal for all production scenarios, it was valuable for exploring how rule changes propagate through the system.
This architecture required more upfront design and refactoring than a feature-first approach. However, the resulting system is significantly more scalable: boundaries are explicit, responsibilities are clear, and future expansion is expected to be faster and safer precisely because the difficult decisions have already been centralized.
These tradeoffs were intentional.
Private project Not licensed for commercial or public distribution.
Made by Guilherme Below Full-Stack Engineer
domain/utilsdomain/play/diceBreakMe.tsx