Browser rhythm game for dance.malldao.xyz. See plan.md for the full roadmap.
Feature / initiative delivery: multi-session work follows docs/process-feature-delivery.md (research → plan → PRD → checkbox tasks → implementation with TDD/BDD). Also referenced from AGENTS.md and .cursor/rules/atdance-workflow.mdc.
- Node.js 20+
- pnpm
| Command | Description |
|---|---|
pnpm install |
Install dependencies |
pnpm dev |
Vite dev server — title → song select (minimal, SynRG, 6jan2002, Forkbomb); chart + audio bytes cache in IndexedDB (idb-keyval) after first fetch |
pnpm build |
Typecheck + production bundle |
pnpm test |
Vitest (unit tests under src/**/*.test.ts and relay/**/*.test.ts). Layered test plans: docs/test-plans.md. |
pnpm test:coverage |
Vitest with coverage report; CI enforces ≥ 50% lines/statements (see vitest.config.ts). |
pnpm e2e |
Playwright smoke against vite preview (run pnpm build first). Installs browsers once with pnpm e2e:install (playwright install chromium). |
pnpm lint |
ESLint |
pnpm format |
Prettier write |
pnpm relay:dev |
Cloudflare Worker — WebSocket ping/pong clock sync (NTP-style); use with Sync Lab (title T) |
┌──────────────────┐ wss (sync) ┌─────────────────────┐
│ Browser (Vite) │ ◄──────────────► │ Cloudflare Worker │
│ Phaser + audio │ │ relay (queue/rooms) │
└────────┬─────────┘ └─────────────────────┘
│ HTTPS
▼
┌──────────────────┐ ┌─────────────────────┐
│ Static host/CDN │ │ Self-hosted PDS │
│ charts + audio │ │ (OAuth, ATProto) │
└──────────────────┘ └─────────────────────┘
- Single-player timing uses the Web Audio clock; calibration adjusts tap offset.
- Sync Lab proves NTP-style offset/RTT over the relay; not real-time versus play (see
plan.md). - Direction: move multiplayer/sync transport toward Holepunch-style P2P (topics, encrypted streams) in the browser — see
docs/architecture-p2p-holepunch.md. The Worker relay remains available during migration.
Copy .env.example to .env.local for local overrides. Important VITE_ vars:
| Variable | Purpose |
|---|---|
VITE_RELAY_WS |
Relay WebSocket URL (wss://…) for production/staging builds (Sync Lab, Mode A legacy). |
VITE_P2P_BOOTSTRAP |
Comma-separated or JSON array of wss:// / ws:// URLs for hyperswarm-web proxy/signal (Sync Lab Mode B). See docs/prd-p2p-sync.md. |
VITE_ATPROTO_PDS_HOST |
Self-hosted PDS / handle resolver base URL when enabling OAuth + score writes. |
pnpm relay:devDefault dev URL is typically ws://127.0.0.1:8787 (see Wrangler output). Point Sync Lab at it via VITE_RELAY_WS before pnpm dev or pnpm build when testing against a deployed Worker.
This repo does not ship docker-compose for a PDS. For self-hosting, follow the official PDS deployment guide (e.g. bluesky-social/pds) and set VITE_ATPROTO_PDS_HOST to your instance.
- Conventional Commits are required (enforced by Commitlint on
commit-msgand on pull requests in CI). Examples:feat: add calibration scene,fix: correct hit window,chore: bump deps. - Releases use release-please via release-please-action (
.github/workflows/release.yml). On pushes tomain/masterit opens or updates a release PR that bumpspackage.jsonandCHANGELOG.mdfrom those commits. Merging that PR creates the GitHub Release and tag. This repo does not publish to npm; add a separate workflow later if you need a registry publish.
?judge=beat— pydance-style BeatJudge (tick windows from BPM).?chart=1— second chart block in a multi-SINGLEfile (e.g. MANIAC insynrg.dance).?e2e=1— Playwright / smoke: skip title, land on song select; drives#e2e-statusmilestones (minimal fixture avoids long audio fetch).?e2e=1&sync_lab=1— boot to Sync Lab (relay); add&sync=p2pfor P2P mode — CI asserts#e2e-statusissync-lab-relayorsync-lab-p2p(seee2e/syncLab.spec.ts).
On the title screen press C (or start the Calibration scene): eight 120 BPM beeps, tap SPACE or click on each beat. The median offset is stored in localStorage (atdance.calibrationOffsetSec) and applied in PlayScene so judging and note scroll use effective time = audio time minus that offset.
The relay/ Cloudflare Worker is optional for browser-to-browser P2P (default Sync Lab P2P path uses hyperswarm-web only). Use the relay when you want the queued pairing / room behavior from Phase 4 or a known wss:// without running a bootstrap server.
Press T on the title screen to open Sync Lab: it connects to the relay WebSocket (dev default ws://127.0.0.1:8787 while pnpm relay:dev is running), sends ten ping messages with UTC t1, receives pong with server t2/t3, and shows per-sample offset / RTT and an EMA of offset. If the socket drops, the UI explains the close reason and you can press SPACE again to reconnect. For staging or production, set VITE_RELAY_WS (e.g. wss://your-worker.workers.dev) before pnpm build. See docs/deployment-vercel-cloudflare.md for Worker deploy.
Use hyperswarm-web in the browser for peer discovery + WebRTC — P2P plan: docs/p2p-plan.md; requirements: docs/prd-p2p-sync.md. Sync Lab can run in P2P mode:
- Set
VITE_P2P_BOOTSTRAPto the basews://orwss://URL of your hyperswarm-web server (default CLI port 4977). Do not include/proxyin the env value — the client appends/proxyand/signalitself. - Run a bootstrap server locally, e.g.
npx hyperswarm-weborpnpm dlx hyperswarm-web(orpnpm exec hyperswarm-webif installed globally). - Open the app with
?sync=p2p(or setVITE_SYNC_LAB_MODE=p2pbefore build). Optional:?topic=my-roomso two tabs share the same topic string (SHA-256 topic key). In Sync Lab P2P: C copies a join link; N picks a new random topic and reloads. - Open two tabs with the same
topic, press SPACE on one (initiator runs 10 NTP samples; the other tab answers pong). Seedocs/architecture-p2p-holepunch.md. Field validation:docs/p2p-sync-lab-manual-qa.md.
The P2P module is lazy-loaded when you run the probe so the default relay build stays smaller until you use P2P.
ICE (STUN / TURN shape): WebRTC uses src/p2p/iceConfig.ts — built-in public STUN URLs; optional extra URLs / TURN objects merge for symmetric NAT (full TURN wiring is P3). Bootstrap is typically one small VPS with hyperswarm-web — see docs/architecture-p2p-holepunch.md and docs/deployment-vercel-cloudflare.md.
NFR (P1 quick notes): N1 — compare median offset vs relay on a good network (same Sync Lab UI). N2 — P2P code stays in the lazy chunk (syncLabP2p-*.js). N3 — treat ?topic= as public (anyone on the topic can join); do not put secrets in topic strings. N4 — run bootstrap on a single low-cost host; document in architecture/deployment docs.
- Convention: colocated
*.test.tsnext to sources (or underrelay/for Worker helpers). - Chart / judge:
.danceparsing,buildNoteTimeline, and judge windows are covered with fixtures and ParseError reports 1-based source line on failure (plan Phase 1.1). - Relay:
relay/src/protocol.tsis unit-tested without deploying (plan Phase 0.6). - Cache:
fetchChartTextCacheduses fake-indexeddb in Vitest for IndexedDB roundtrip (plan Phase 2.2). - E2E:
e2e/smoke.spec.tsloads/?e2e=1and drives play via#e2e-status;e2e/syncLab.spec.tsloads Sync Lab relay + P2P hooks. CI runspnpm build→playwright install chromium --with-deps→pnpm e2e.
docs/performance-budget.md— manual performance checklist (Phase 5.2).docs/operator-runbook.md— deploy, relay, OAuth rotation (Phase 5.3).docs/release-checklist.md— pre-release and acceptance (Phase 5.4).
- pydance (DDR-style game and
.danceformat) — the main design reference for this project’s chart timing (toRealTime/ beat units), note timeline progression (stops, BPM), TimeJudge / BeatJudge windows, Dance Points grading, and panel digit semantics. See inline references insrc/chart/dance/,src/judge/, andsrc/scoring/dancePoints.ts. Community mirrors exist on GitHub (e.g. forks of the historical tree); the behavior ATDance matches is that of the classic pydance/pyddr implementation.
- Root:
package.json, Vite, ESLint, Prettier, Vitest src/— Phaser appsrc/chart/dance/—.danceparser +buildNoteTimeline(pydance-compatible math; tested)src/cache/—fetchChartTextCached/decodeAudioFromUrlCached(IndexedDB viaidb-keyval)src/audio/—AudioClock,AudioScheduler(Web Audio–relative timing)src/scenes/SongSelectScene.ts— pick chart + difficulty index; passesPlaySceneDatatoPlayScenesrc/scenes/ResultsScene.ts— Dance Points summary (pydance-style) after a chart completes; R → song selectsrc/scoring/dancePoints.ts—grades.pyDance Points weights + letter gradesrc/load/httpSongLoader.ts—{base}/{songId}/{songId}.dancefetch helper (Phase 2.1)src/songSelect/songSelectRows.ts— built-in demo list (testable)relay/— Cloudflare Worker (Wrangler)
StepMania .sm / .ssc support is post-MVP (see plan.md).
- Deploy (Vercel + Cloudflare Worker + DNS): step-by-step guide for beginners:
docs/deployment-vercel-cloudflare.md. Lowest $ / lowest ops:docs/deployment-shoestring.md. - Static app: target production URL
https://dance.malldao.xyz(or Vercel / Cloudflare Pages). CI builds a staticdist/; deploy that folder. - Relay Worker: deploy
relay/with Wrangler; setVITE_RELAY_WS/VITE_PUBLIC_RELAY_WSto the deployedwss://URL. Cold starts clear in-memory queue state; acceptable for MVP sync proof (seeplan.mdAppendix B). - Demo charts on disk (maintainer): reference pydance tree
beatgen/pydance/songs/(SynRG, 6jan2002, Forkbomb). This repo ships a minimal subset underpublic/songs/for HTTP loading; do not add large binaries without checking license. - Env template:
.env.examplelistsVITE_RELAY_WSand optionalVITE_ATPROTO_PDS_HOST.