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,即可驗證所有權並自行管理介紹。
一分鐘完成驗證The ID Game
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
A real-time party game for nights out — play it live at id-game.com.
One player secretly picks a scenario ("most likely to argue with their parents over something ridiculous…"), ranks everyone in the room by it, and the rest have to guess which scenario it was. Create a room, share the six-character join code, and play.檢視完整 README ↗A real-time party game for nights out — play it live at id-game.com.
One player secretly picks a scenario ("most likely to argue with their parents over something ridiculous…"), ranks everyone in the room by it, and the rest have to guess which scenario it was. Create a room, share the six-character join code, and play.
| Layer | Choice |
|---|---|
| Frontend | Next.js (App Router) · Tailwind · shadcn/ui · Dice UI |
| Data | Convex — reactive real-time database; game state syncs to all clients live |
| Auth | Better Auth running on Convex — passkeys first, email one-time codes as fallback (delivered by Resend) |
| Analytics | PostHog |
| Hosting | Vercel (app) + Convex Cloud (backend) |
There are no passwords. Accounts exist mainly to keep bots from creating and abandoning games, so sign-in is deliberately light — and people who only want to join a friend's game don't need one at all:
createGame mutation refuses any caller whose JWT says isAnonymous. Guest sign-ins are rate-limited per IP (10 a minute).Mechanically:
Browser ── /api/auth/* (Next.js catch-all route) ──▶ Convex HTTP actions (Better Auth core)
│ │
└── Convex queries/mutations (JWT) ◀── trusts ───────────┘
@convex-dev/better-auth (local-install component); auth tables live in Convex alongside game data.app/api/auth/[...all] proxies auth requests to Convex, so sessions are same-origin cookies on the app domain.ctx.auth.getUserIdentity() — identity.subject is the Better Auth user id, and is what players.userId / games.createdBy store. The JWT also carries the user row's fields, so identity.isAnonymous tells guests apart without a lookup.proxy.ts gates /game* on session-cookie presence (optimistic, fast); guests and accounts have the same cookie, so the authoritative checks are in the Convex functions.Key auth files: convex/auth.ts (Better Auth config + plugins), convex/http.ts (route registration), lib/auth-client.ts / lib/auth-server.ts (client/server helpers), app/(auth-pages)/sign-in/ (the single auth page, static and without the Convex client), components/guest-join.tsx (the invite page's guest form), convex/guests.ts (moves a guest's seats onto their new account).
flowchart TD
link(["Invitee opens shared link /game/CODE"]) --> proxy{"proxy.ts:<br/>session cookie?<br/>(guest or account)"}
play(["Play → /game"]) --> proxy
proxy -- "no, invite link" --> invite["/join/CODE<br/>public invite page"]
proxy -- "no, any other /game path" --> signin
proxy -- yes --> gamePage
invite --> open{"Game still<br/>open?"}
open -- "no, started" --> signin
open -- yes --> remembered{"localStorage:<br/>account signed in<br/>here before?"}
remembered -- yes --> modal["'Welcome back' modal"]
modal -- "Sign in" --> signin
modal -- "Join as a guest" --> anon
remembered -- no --> guestButton["Invite card:<br/>'Join as a guest'"]
guestButton --> anon["POST /sign-in/anonymous<br/>(10/min per IP)<br/>→ 'Guest 1234',<br/>isAnonymous: true"]
anon -- "full page load" --> gamePage
signin["/sign-in?next=…<br/>passkey or email code"] -- "full page load<br/>to next" --> gamePage
gamePage{"Which page?"}
gamePage -- "/game/CODE" --> join["Server: isPlayerInGame,<br/>then joinGame if not a player"]
gamePage -- "/game" --> lobby["Create / join screen"]
lobby -- "Create New Game" --> isGuest{"Guest?"}
isGuest -- no --> create["createGame mutation<br/>(rejects isAnonymous)"]
isGuest -- yes --> upsell["'Hosting needs an account'<br/>→ /sign-in?tab=sign-up"]
upsell --> signin
join --> named{"Still<br/>'Guest 1234'?"}
named -- yes --> prompt["Lobby name prompt<br/>(DisplayNamePrompt)"]
named -- no --> game(["In the game"])
prompt --> game
create --> game
Guests name themselves in the lobby rather than on the invite page, so nothing stands between the link and the game: DisplayNamePrompt asks anyone still on their generated "Guest 1234", the same way it asks an account that has no name, and the name it saves is copied onto the cards they already have.
The "Welcome back" prompt stops a returning player from joining as a stranger to their own account. components/remember-account.tsx records a first name (never the email) in localStorage whenever an account — not a guest — is signed in, and signing out leaves it in place, because "this device has an account" is exactly what the invite page wants to know.
A guest who signs in or signs up (from the user tray's "Create account", or the hosting prompt) keeps their seats. The anonymous plugin's after-hook sees the old guest session on the same request that creates the real one, and onLinkAccount moves the guest's players rows across before the plugin deletes the guest user. If the move fails, the sign-in fails with it, so the guest and their seats survive to retry.
sequenceDiagram
autonumber
actor G as Guest browser
participant N as Next.js /api/auth
participant BA as Better Auth (Convex HTTP action)
participant DB as Convex DB
G->>N: Passkey or email-code sign-in<br/>(guest session cookie attached)
N->>BA: proxied request
BA->>DB: verify credential, create account session
BA->>BA: anonymous after-hook finds the guest session
BA->>DB: onLinkAccount: a nameless new account takes the guest's name
BA->>DB: runMutation(guests.adoptGuestPlayers)<br/>players.userId: guest id → account id
BA->>DB: delete guest user and its sessions
BA-->>N: Set-Cookie: account session + Convex JWT
N-->>G: response
G->>G: full page load to next
If the account already has its own seat in one of those games, the account's seat wins and the guest's is retired (active: false), the same way consensus removal retires a player.
Guests who never sign up are cleaned up by the daily delete expired guests Convex cron (deleteExpiredGuests in convex/cleanup.ts): once a guest is 30 days old and every one of its sessions has expired, nobody can sign in as it again, so the user and its sessions are deleted. Its players rows stay, as game history. The admin users page counts guests separately from accounts.
Assumes you know Convex and Next. From a fresh clone:
pnpm install
npx convex dev # terminal 1 — creates/attaches a dev deployment
pnpm dev # terminal 2
.env.local:
# Convex (written by `npx convex dev` on first run)
CONVEX_DEPLOYMENT=<your-convex-deployment>
NEXT_PUBLIC_CONVEX_URL=<your-convex-url>
NEXT_PUBLIC_CONVEX_SITE_URL=<same-as-convex-url-but-.site>
# App
NEXT_PUBLIC_SITE_URL=http://localhost:3000
# Maintenance mode (optional)
MAINTENANCE_MODE=false
MAINTENANCE_BYPASS_SECRET=<random-string-16+-chars>
# PostHog
NEXT_PUBLIC_POSTHOG_KEY=<your-posthog-key>
NEXT_PUBLIC_POSTHOG_API_HOST=/ingest # DO NOT CHANGE
NEXT_PUBLIC_POSTHOG_UI_HOST=<your-posthog-host-url>
Auth env vars live on the Convex deployment, not in .env.local:
npx convex env set BETTER_AUTH_SECRET $(openssl rand -base64 32)
npx convex env set SITE_URL http://localhost:3000
npx convex env set RESEND_API_KEY <your-resend-key>
npx convex env set AUTH_EMAIL_FROM "The ID Game <[email protected]>"
The game needs rows in the scenarios table to be playable — seed some via the Convex dashboard if your deployment is fresh. Schema lives in convex/schema.ts.
npx convex deploy pushes the backend. Set the four Convex-side env vars (above, with production values) before deploying — the backend fails fast with a SITE_URL is not set error otherwise.MAINTENANCE_MODE=true in Vercel and redeploy — every route returns a 503 "back soon" page (crawler-correct, Retry-After set). Visit any URL with ?bypass=<MAINTENANCE_BYPASS_SECRET> to set a cookie that lets you through for smoke-testing while the curtain is up. Flip back to false and redeploy to reopen.NEXT_PUBLIC_* + maintenance vars live in Vercel; BETTER_AUTH_SECRET, SITE_URL, RESEND_API_KEY, AUTH_EMAIL_FROM live on the Convex deployment (npx convex env set --prod …).JWKS set on the Convex deployment, token verification skips the self-request to /api/auth/convex/jwks and minting skips the jwks table read. Unset, both work as before. To set it, run npx convex run auth:getLatestJwks --prod | npx convex env set JWKS --prod, then npx convex deploy: auth.config.ts is read at push time, so the redeploy is what switches verification over. This reuses the newest existing key, so sessions carry on. The value contains the private key, so never log or commit it. Rotation becomes manual. rotateKeys is deliberately not exposed, so rotating means adding a one-off internal action that calls auth.api.rotateKeys(), running it, then setting JWKS and redeploying as above. Tokens signed with the old key stop verifying, which forces re-auth for up to 15 minutes. To turn it off, deploy with JWKS still set but the static wiring reverted, then remove the variable. Removing it first leaves the deployed config expecting static keys that the plugin no longer has, which breaks sign-in until the next deploy.