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,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Dungeons and Demons
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
A campaign companion for tabletop groups: sign in, roll up characters, and keep
their sheets in one place.
An npm workspace monorepo with two packages:
Sina has no dependency on Next.js. It owns the Supabase clients, every data
query, and every validation rule; where something genuinely needs the framework
— the request-scoped cookie store, for instance — Maria passes it in.檢視完整 README ↗A campaign companion for tabletop groups: sign in, roll up characters, and keep their sheets in one place.
An npm workspace monorepo with two packages:
| Package | Role |
|---|---|
| Maria | Frontend — the Next.js 16 application, React 19, Tailwind CSS v4 |
| Sina | Backend — database schema, data access, and the rules that guard it |
Sina has no dependency on Next.js. It owns the Supabase clients, every data query, and every validation rule; where something genuinely needs the framework — the request-scoped cookie store, for instance — Maria passes it in.
The one Supabase call Maria makes for itself is auth.getClaims(), in
proxy.js and in lib/supabase.js, because the token has to be verified
before anything can be decided about the request. Even there the decision stays in
Sina: authCouldNotAnswer and resolveRedirect in src/supabase/session.js.
That keeps the backend portable and makes the seam between the two obvious
rather than implied.
node --version. npm ships
with it and any version this recent has the workspace support we need.git clone https://github.com/Attack-on-Main-Branch/DnD.git
cd DnD && npm install
One install at the root covers both workspaces.
cp Maria/.env.example Maria/.env.local
Environment files belong to Maria, because that is where Next looks for them. Both values come from your project's API settings in the Supabase dashboard:
| Variable | What to paste |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | The project URL — the bare origin, e.g. https://abcd1234.supabase.co |
NEXT_PUBLIC_SUPABASE_ANON_KEY | The publishable key (sb_publishable_…), or on an older project the anon JWT |
Use the bare project URL, with no
/rest/v1/on the end. The client appends its own paths, so a trailing path sends every auth call to a URL that does not exist.
Either key format works — the pinned @supabase/supabase-js recognises both,
and nothing in this repo inspects the value. Both are meant to reach the
browser, and only grant what your Row Level Security policies allow.
Never put the secret key — sb_secret_…, or the legacy service_role JWT
— in this file or in any NEXT_PUBLIC_ variable. It bypasses RLS entirely.
The scene painter (the camera on the Dungeon Master's rail) paints with Gemini on Google Cloud's Vertex AI, billed to the project's billing account (free-trial credits included). In the Google Cloud console, pick the project and enable the Vertex AI API under APIs & Services → Library. The server then signs in by the first of these it finds; with none, the rest of the app works and painting says it is not set up.
On your own machine — no key. Install the Google Cloud CLI, then:
gcloud auth application-default login
gcloud auth application-default set-quota-project YOUR_PROJECT_ID
Set VERTEX_PROJECT_ID=YOUR_PROJECT_ID in Maria/.env.local and restart the dev
server. The app reads the login gcloud saved; when it expires, painting says so
and the first command signs you in again.
On Vercel — no key either, through Workload Identity Federation. A local gcloud login does not travel with the deployment.
In the Google Cloud console, select the project and confirm its Billing account is active. Under APIs & Services → Library, enable Vertex AI API, Identity and Access Management (IAM) API, Cloud Resource Manager API, IAM Service Account Credentials API, and Security Token Service API. Note the Project ID and Project number under IAM & Admin → Settings.
In Vercel, note the team slug from the team's URL and the project name under
Project → Settings → General. Under Settings → Security → Secure backend
access with OIDC federation, enable OIDC and check its issuer mode: the
configuration below uses Team, with issuer
https://oidc.vercel.com/YOUR_TEAM_SLUG. A project using Global instead
needs issuer https://oidc.vercel.com in the next step.
In Google Cloud, open IAM & Admin → Workload Identity Federation → Create
pool, with pool ID vercel and an OpenID Connect provider with ID
vercel. Enter the issuer from step 2, select Allowed audiences and enter
https://vercel.com/YOUR_TEAM_SLUG. Leave the JWK file empty and map
google.subject to assertion.sub. Use Allowed audiences for the current
code, which requests Vercel's standard audience.
Under IAM & Admin → Service accounts → Create service account, create
scene-painter with the Vertex AI User role (roles/aiplatform.user)
on the project. Do not create a key. Open that service account's
Permissions → Grant access and grant Workload Identity User
(roles/iam.workloadIdentityUser) to this principal, replacing the three
placeholders with the project number, Vercel team slug and Vercel project name:
principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/vercel/subject/owner:YOUR_TEAM_SLUG:project:YOUR_VERCEL_PROJECT:environment:production
In Vercel → Project → Settings → Environment Variables, add these for the Production environment. Use the app's names below, rather than the names in Vercel's general examples:
| Variable | Value |
|---|---|
VERTEX_PROJECT_ID | The project ID |
VERTEX_WIF_PROJECT_NUMBER | The project number (IAM & Admin → Settings) |
VERTEX_WIF_POOL_ID | The pool's ID, e.g. vercel |
VERTEX_WIF_PROVIDER_ID | The provider's ID, e.g. vercel |
VERTEX_SERVICE_ACCOUNT_EMAIL | The service account's email |
Maria/vercel.json enables Fluid
Compute and the table page sets maxDuration = 120; Fluid Compute supports
this on Hobby too. Open a campaign as its
Dungeon Master, place a camera and paint a scene. Confirm the image appears
on the map shelf. If it fails, inspect the deployment's runtime logs for
paintScene: setup errors indicate credentials/API permissions, rejected
requests indicate the model/request, and timeouts indicate duration limits or
a slow upstream request.Preview deployments need their own environment variables and an additional
service-account binding ending in environment:preview; the production binding
does not authorise previews. If using a different pool/provider ID, substitute it
in the principal and environment variables. Keep the existing Supabase variables
pointing at the database where the scene migrations were applied.
The console setup follows Vercel's GCP federation guide and Google's federation prerequisites.
Or with a service account key, where your organisation allows creating one:
put the JSON key, base64-encoded, in VERTEX_SERVICE_ACCOUNT_KEY. It takes
precedence over the other two.
VERTEX_LOCATION (default global) and VERTEX_IMAGE_MODEL (default
gemini-3.1-flash-image; gemini-3-pro-image is sharper but slower and about
twice the price) are optional everywhere. All of these are read on the
server alone: never give one a NEXT_PUBLIC_ prefix.
The schema lives in Sina/supabase/migrations/. Pick either route.
Option A — paste the SQL (no extra tooling). In the Supabase dashboard open SQL Editor → New query, then paste and run each file in filename order:
20260811140554_characters.sql — the characters table, RLS policies, the
unique name#tag index and the three-per-account trigger20260811141732_revoke_trigger_function_execute.sql — revokes the public
EXECUTE grant on the trigger function20260811144707_character_color_and_level.sql — adds color_theme and
level20260814195921_character_class.sql — adds archetype and class_id, with
the paired characters_class_check constraint20260814215246_race_check_and_limit_lock.sql — constrains race to the
nine playable races, and takes a per-user advisory lock in the character
limit trigger so concurrent inserts cannot race past it20260816120000_limit_trigger_scope.sql — stops that trigger counting rows
for an insert that is not the caller's own, which otherwise answered whether
any account you could name had already used its three slotsOrder matters more than it looks. Every file is safe to re-run on its own — tables, indexes and columns are guarded, each policy is dropped first, and each constraint is wrapped in an existence check. But files 1, 5 and 6 each
create or replacethe same function,enforce_character_limit, with a different body. Going back and re-running an earlier one after a later one silently downgrades the live function, with no error: re-running file 1 strips both the advisory lock and the ownership guard; re-running file 5 strips the guard and reopens the leak file 6 closes. After any out-of-order paste, finish by running the highest-numbered of those three again.
Option B — the Supabase CLI. Already a dev dependency of Sina, and it tracks which migrations have been applied, so it cannot make the mistake above. One-off setup:
npx supabase login
cd Sina && npx supabase link --project-ref <your-project-ref> && cd ..
Then, now and whenever migrations change:
npm run db:push
The link is per-machine — the CLI stores the project reference in
Sina/supabase/.temp, which is git-ignored — so everyone runs link once for
themselves.
In Authentication → Providers → Email:
npm run dev
Open http://localhost:3000. There is no landing page — / sends you to
/login, or to /dashboard if you already have a session.
The database is not on anybody's machine. It is hosted by Supabase, and
Maria/.env.local decides which database the app talks to. That one file
is the whole answer to "will I see the account they just created?".
Each developer with their own Supabase project is the better default. They create a free project, run the migrations, and fill in their own two values. Nothing they do is visible to you, and they try a migration out on their own data before it ever reaches yours.
Sharing one project means handing over your two values, and skipping steps 4 and 5 — the schema is already there. Every account and character is then common to both of you. Worth knowing before choosing it: you are both working against real data, and a migration pushed by either of you lands for both.
The publishable/anon key is safe to hand to a collaborator — it reaches every visitor's browser anyway, and RLS is what actually protects the rows. It is still not something to post in public: anyone holding it can create accounts in your project. The secret key is a different matter and is never shared.
Changing the schema on a shared project needs more than the key: the CLI
authenticates as you, so a collaborator has to be invited to the project in
the Supabase dashboard before npx supabase link will work for them.
Run these from the repository root; each delegates to the right workspace.
| Command | What it does |
|---|---|
npm run dev | Development server with hot reload |
npm run build | Production build |
npm start | Serve a production build |
npm test | Sina's test suite, on node --test |
npm run lint | ESLint, both workspaces |
npm run format | Prettier, writing changes |
npm run format:check | Prettier, reporting rather than writing |
npm run db:push | Apply pending migrations to the linked project |
npm run db:new | Scaffold a new migration file |
npm run db:list | Compare local migrations against the remote |
npm run db:list is the one that catches a migration committed but never
pushed. Nothing else connects the two: the tests run against stubs and never
touch SQL.
Serving a production build over plain HTTP will not keep you signed in on any address except localhost: auth cookies are marked
secureoutside development, and browsers only make an exception for localhost, which they treat as a trustworthy origin. Sonpm startis fine at http://localhost:3000, and reaching that same server from another machine on the network is not — use HTTPS for that.
Maria/ frontend
app/
login/ sign-in and sign-up views, auth Server Actions
dashboard/ character roster, creation flow, settings
character/[id]/ character sheet with tabs
race-art/ card artwork, imported as modules
error.jsx error boundary for the signed-in area
components/ header, changelog panel, the animated mark
ui/ shared primitives — Button, TextField, SelectMenu, …
brand/ the grimoire mark itself
paths-background/ the animated background and its renderer
icon.png, apple-icon.png the app icons, by Next's file conventions
lib/supabase.js the Next cookie adapter, and the current-user lookup
lib/errors.js failure logging, shared by actions and pages
proxy.js session refresh and route protection
Sina/ backend
src/rules/ what is valid: auth, character, account
src/data/ every query and mutation
src/supabase/ client construction, access policy
src/*.test.js the suites, beside the code they cover
src/supabase-stub.js the fake query builder those suites run against
supabase/migrations/ database schema, in order
assets/ source artwork, git-ignored (see below)
A few decisions worth knowing before changing things.
handle_taken is a backend
fact; "That name and tag are already taken" is a product decision. Wording
can change without anyone touching a query.proxy.js, not middleware.js. Next 16 renamed the convention. The old
name still works but warns on every build.getClaims(), never getSession(), on the server. Only the former
verifies the JWT; the latter just reads a cookie. With the project's
asymmetric signing key the check is local, so no request waits on Auth for
it.authCouldNotAnswer draws the line once, and the proxy lets
those requests through rather than diagnosing them — the page then verifies
for itself and throws into its error boundary.name#tag uniqueness is an index — an API sits in front of
the Server Actions, so application code cannot be the only guard.Maria/app/dashboard/race-art.
The images are the same for every visitor, so they belong in the repository;
the characters row stores only the race, and character-presentation.js
maps it to a file by importing each .webp as a module rather than naming a
path under public/. That puts a content hash in the URL, so replacing a
picture cannot leave a stale one in anybody's cache. /assets/ holds the
full-resolution originals and is git-ignored, so a clone will not have it.Maria/app/components/changelog.js
feeds the panel behind the mark in the dashboard's corner, and it is meant to
gain an entry with every commit — a commit subject is written for whoever
maintains the code, not for whoever uses the app.Sign-up currently expects Confirm email to be off, which is why a new account lands straight on the dashboard.
If you switch it on, Supabase emails a link containing a one-time token. That
link needs a route in Maria — typically app/auth/confirm/route.js, which does
not exist yet — calling supabase.auth.verifyOtp({ token_hash, type }) to
exchange the token for a session. You will also need to pass emailRedirectTo
in the signUp call — Sina/src/data/auth.js, which does not pass it today.
Then allow-list that redirect under Authentication → URL Configuration →
Redirect URLs. Entries are matched against the full URL, so a bare origin
does not authorise …/auth/confirm — add the whole path, or a wildcard such as
http://localhost:3000/**. An unmatched value is silently replaced by the Site
URL, which looks like the link simply not working.
Note that Supabase's built-in email sender is for development only: it is heavily rate-limited and may only deliver to your own team's addresses. Production needs custom SMTP under Project Settings → Authentication → SMTP.
Import the repository and set the root directory to Maria. That is where
the Next app lives, and it is what Vercel uses to detect the framework and find
the build output; the root npm run build only delegates to Maria anyway.
Maria depends on sina as a workspace package, resolved through a symlink the
root install creates — so the install has to run with the repository root in
view. If a build fails to resolve sina, that is the setting to check first.
Add NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY under the
project's Environment Variables, and the scene painter's Workload Identity
Federation variables (see step 3 above). A painting can take over a minute, and
the table page asks for a maxDuration of 120 seconds. Maria/vercel.json enables
Fluid Compute; its duration limits support this on all plans, including Hobby.
See Vercel's duration documentation.
If email confirmation is on, add the
deployment's full /auth/confirm URL to Supabase's Redirect URLs as well.
VERTEX_*GCP_*