{"id":51344193,"url":"https://github.com/nilicule/rave","last_synced_at":"2026-07-02T10:04:10.713Z","repository":{"id":360991588,"uuid":"1252622878","full_name":"nilicule/rave","owner":"nilicule","description":"A massive, multiplayer rave","archived":false,"fork":false,"pushed_at":"2026-05-28T18:16:45.000Z","size":73,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-28T19:27:09.357Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nilicule.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-28T17:51:41.000Z","updated_at":"2026-05-28T18:17:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nilicule/rave","commit_stats":null,"previous_names":["nilicule/rave"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/nilicule/rave","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nilicule%2Frave","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nilicule%2Frave/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nilicule%2Frave/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nilicule%2Frave/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nilicule","download_url":"https://codeload.github.com/nilicule/rave/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nilicule%2Frave/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35042022,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-02T02:00:06.368Z","response_time":173,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-02T10:04:10.146Z","updated_at":"2026-07-02T10:04:10.704Z","avatar_url":"https://github.com/nilicule.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rave.world\n\nA tiny web-based, massively-multiplayer online rave. Three.js in the browser, FastAPI WebSockets on the server, no build step.\n\n## Run it\n\nPrereqs: `uv` (https://docs.astral.sh/uv/) and Python 3.13+.\n\n```bash\nuv sync\nuv run uvicorn main:app --reload\n```\n\nOpen http://localhost:8000 in a browser. Open more tabs to see multiplayer.\n\n**Controls**\n\n- **W / S** — walk forward / backward (in the direction your character is facing)\n- **A / D** — turn the character left / right\n- **Click** the scene — engage mouselook (pointer lock). Mouse then orbits the camera around your character (yaw + pitch).\n- **Esc** — release the mouse (so you can click the YouTube controls or another tab)\n\n## Config\n\n### Server\n\n| Constant | Where | Default | Notes |\n|---|---|---|---|\n| `BROADCAST_HZ` | `server/connection_manager.py` | `20` | State snapshot rate to all clients |\n| `SPAWN_RADIUS` | `server/player.py` | `8.0` | Players spawn inside this square around origin |\n| host / port | passed to `uvicorn` | `127.0.0.1:8000` | `--host 0.0.0.0 --port 8000` to expose on the network |\n\n### Client\n\nAll in `static/js/config.js`:\n\n| Constant | Default | Notes |\n|---|---|---|\n| `VIDEOS` | `[]` | List of bare YouTube video IDs. Looped individually. |\n| `PLAYLISTS` | three Drumcode playlists | `{ 'Label': 'PL...id' }`. Shuffled and started at a random index. |\n| `MOVEMENT_SPEED` | `4.5` | World units per second |\n| `NETWORK_SEND_HZ` | `15` | How often the client pushes its position |\n| `INTERP_DELAY_MS` | `120` | Render-behind window for remote-player smoothing |\n| `STAGE_WIDTH` / `STAGE_HEIGHT` / `STAGE_Z` / `STAGE_Y_CENTER` | 14 / 7.875 / 12 / 4.5 | Stage screen geometry |\n\nOn load, each tab picks **one** entry uniformly at random across `VIDEOS + PLAYLISTS` — no cross-client sync. A single video and a single playlist count equally regardless of how many videos the playlist contains.\n\n**Sound:** the stage iframe is muted at load (browsers block autoplay-with-audio without a user gesture). The first click on the scene (the same one that engages pointer lock) calls `unMute()` on the player.\n\nThe WebSocket URL is derived from `window.location` (`ws://` or `wss://` automatically), so changing the host/port doesn't need code changes.\n\n## Architecture\n\n```\nmain.py                       FastAPI app + WS endpoint + static mount\nserver/\n  protocol.py                 Pydantic message models, MessageType enum\n  player.py                   Player dataclass (server-side state)\n  connection_manager.py       WS registry + broadcast loop\nstatic/\n  index.html                  Importmap for Three.js, WebGL + CSS3D hosts\n  js/\n    config.js                 All tunables\n    protocol.js               Mirrors server MessageType\n    scene.js                  Renderers, ground, sky, stars, moon\n    avatar.js                 Blocky humanoid factory (limb refs on userData)\n    stage.js                  Stage backing + CSS3D iframe overlay\n    lighting.js               Animated club spotlights / point lights\n    input.js                  WASD keyboard state\n    localPlayer.js            Client-side movement + camera follow\n    remotePlayers.js          Registry + buffered interpolation\n    network.js                WebSocket client + throttled send loop\n    main.js                   Entry point: wires it all up\n```\n\n### Message protocol (wire)\n\nAll messages are JSON with a `type` field. See `server/protocol.py` for canonical models; `static/js/protocol.js` mirrors the constants.\n\n- **S→C `welcome`** — sent once per connect. `{ your_id, you, players }`\n- **S→C `join`** — broadcast when someone connects. `{ player }`\n- **S→C `leave`** — broadcast when someone disconnects. `{ player_id }`\n- **S→C `state_update`** — broadcast at `BROADCAST_HZ`. `{ players: [...] }`\n- **C→S `player_move`** — sent at `NETWORK_SEND_HZ`. `{ position, rotation }`\n\nAdding a new C→S message kind: define a Pydantic class in `protocol.py`, append it to the `ClientMessage` discriminated union, mirror its `type` value in `static/js/protocol.js`, and add a branch in `main.py`'s `websocket_endpoint` loop.\n\n### Movement model\n\nClient owns its position. Each frame, `localPlayer.js` reads keyboard + mouse intent:\n\n- **A/D** and **mouse-X** both feed character yaw. A/D integrates at `TURN_SPEED` rad/s; mouse-X applies `dx * MOUSE_YAW_SENSITIVITY` directly. Same effect, two input modes.\n- **W/S** moves along the character's forward vector (`(sin yaw, 0, cos yaw)`) at `MOVEMENT_SPEED`.\n- **Mouse-Y** tilts the camera pitch (`PITCH_MIN..PITCH_MAX`). No keyboard analog.\n\nThe camera arm is always directly behind the character (`characterYaw + π`); pitch tilts it up/down. The look-at point sits at chest height on the character. Camera position lerps with frame-rate-independent exponential smoothing.\n\nThe network layer reads a snapshot at `NETWORK_SEND_HZ` and emits `player_move` — independent of the render frame rate. Only the character yaw goes over the wire; camera pitch is purely local.\n\nRemote players are rendered `INTERP_DELAY_MS` behind the latest server snapshot, interpolating between the two surrounding samples. This trades a bit of latency for smooth motion at any frame rate.\n\n## Design choices\n\n### FastAPI (not raw `websockets`)\n\nFastAPI gives us one ASGI app for both `/ws` and the static frontend, served on one port by one `uvicorn` command. Pydantic models are native and the discriminated-union protocol \"just works\" via `TypeAdapter`. The raw `websockets` library would need a separate HTTP server (or hand-rolled one) for the static files, splitting the runtime and URL space — not worth it for this scope.\n\n### YouTube via CSS3DRenderer + iframe (not `VideoTexture`)\n\nYouTube blocks cross-origin access to its `\u003cvideo\u003e` element, so a Three.js `VideoTexture` of a YouTube embed isn't possible — you'd need a self-hosted MP4 to use that path. The robust alternative is Three.js's `CSS3DRenderer` addon: an iframe is wrapped in a `CSS3DObject`, placed in the same world-space coordinates as the stage plane, and renders with the same camera projection as the WebGL scene. The stage iframe stays anchored to its 3D position as the player walks around.\n\nThe trade-off: CSS3D content always layers above the WebGL canvas. A player walking between the camera and the stage would appear *behind* the iframe. Acceptable here — the camera stays behind the player facing the stage.\n\n### Unified turning: A/D and mouse-X both rotate the character\n\nA/D rotates the character (not strafe), W/S moves along its facing vector. Mouse-X feeds into the same character yaw, so dragging right is \"turn right\" exactly like holding D — just continuous instead of discrete. The camera sits directly behind the character and inherits any turning automatically. Mouse-Y is the only thing that's camera-only: it tilts pitch. Remote players only see `position` and `rotation` (character yaw); camera pitch stays client-side.\n\nThe iframe inside the CSS3D layer captures clicks once pointer lock is released — Esc, then click the YouTube embed if you want to use its native controls.\n\n## Deployment notes (for later)\n\nThe code is local-only right now. `main.py` and the frontend both carry `TODO` markers near things that need to change:\n\n- Bind uvicorn to `0.0.0.0` (or run it behind a reverse proxy).\n- Terminate TLS in front; the frontend already auto-upgrades to `wss://` when served from `https://`.\n- Move host / port / `YOUTUBE_VIDEO_ID` / log level to environment variables.\n- Serve static assets through a CDN / reverse proxy instead of FastAPI's `StaticFiles`.\n- Add reconnect-with-backoff in `network.js`.\n- Add bounds / collision in `localPlayer.js`.\n\n## Next features (architectural seams)\n\n- **Dancing.** `avatar.js` exposes limb pivot groups on `userData.limbs`. Add an `animation_state` field to `PlayerState` (server + client mirrors), drive limb rotations from it in a per-frame update. The seam exists; no rewiring needed.\n- **Chat.** Add `ChatMessage` to `protocol.py`, append to `ClientMessage`, mirror in `protocol.js`, dispatch in `network.js`. Render bubbles above avatars in `remotePlayers.js` / `localPlayer.js`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnilicule%2Frave","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnilicule%2Frave","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnilicule%2Frave/lists"}