Inscribe the sigil. Cross the threshold. Duel by prompt.
PromptCast is a browser-based, peer-to-peer 3D spell duel where players cast in natural language. A local LLM (loaded entirely in-browser via cogentlm) interprets each prompt into a structured, validated spell — VFX, physics, audio, and balance bake out into a runnable scene the engine fires across the network.
Project site: https://www.cogentlm.com
- The Game
- Tech Stack
- The
cogentlmPackage - Quick Start
- Environment Variables
- Secrets & Security
- Project Structure
- Multiplayer & Networking
- Debugging & Developer Tools
- Build & Deploy
- Contributing
- Links
Lore-flavored pitch. Two wizards meet at the threshold. One inscribes a sigil; the other speaks it. Inside the arena they trade incantations — the words you write are the spell. The Sage (the model) listens, weighs your intent, and shapes a working incantation in the world.
Technical pitch. PromptCast is an AI-driven 1v1 spell battler built on Next.js + React Three Fiber + Rapier. Players type free-form prompts; a constrained, multi-call LLM pipeline produces a JSON spell (concept → balance) validated against a Zod schema, then handed to the runtime to drive VFX (three.quarks), rigid-body projectiles (Rapier), and audio (ElevenLabs). Multiplayer is P2P over WebRTC via PeerJS, with the host as authority for player list and room state.
Match flow: Lobby → Inscribe Sigil → Cross Threshold → Begin Duel → Cast Spells → Resolve.
| Domain | Library |
|---|---|
| Framework | Next.js (App Router), React 19, TypeScript |
| 3D / Rendering | three, @react-three/fiber, @react-three/drei, @react-three/postprocessing, three.quarks |
| Physics | @dimforge/rapier3d-compat (pinned 0.19.2), @react-three/rapier |
| Networking | peerjs (WebRTC) |
| State | zustand |
| Validation | zod |
| AI | cogentlm (in-browser LLM, GGUF via OPFS) |
| Audio | ElevenLabs (server route) |
| IDs | nanoid |
cogentlm is the in-browser LLM client this project uses to turn natural-language prompts into structured spells. The engine runs in a worker, downloads a pinned GGUF model into OPFS on first load, and exposes a streaming, grammar-constrained query API.
How PromptCast uses it:
src/game/ai/cogentSpellGenerator.ts— owns theCogentEnginesingleton, OPFS model registration/caching, progress fan-out to the loader UI, and observability subscription. The pinned model isLFM2.5-1.2B-Instruct-Q4_K_M.gguf(seeMODEL_URLatsrc/game/ai/cogentSpellGenerator.ts:35).src/game/ai/pipeline/— multi-call pipeline that streams a spell through stages (conceptCall.ts,balanceCall.ts) usingcallRunner.ts. Each stage emitsPipelineProgressevents the UI renders live.src/game/ai/grammar/— JSON grammar definitions used to constrain model output to valid spell shapes.src/game/spells/spellSchema.ts— Zod schema the pipeline output is validated against before reaching the runtime.src/game/ai/spellLog.ts— stage-tagged logger for inspecting pipeline runs.src/game/ai/spellGenerationError.ts— typed error wrapper exposingstage+canRepairso the UI can offer a Repair retry.
Browser requirements (inferred from the loader):
- Secure context with WebAssembly + Worker support
- OPFS (
navigator.storage.getDirectory) for model caching - Plenty of free disk space for the GGUF (~700MB+ depending on quant)
- Cross-origin isolation may be required depending on cogentlm's worker mode
See cogentlm.com for SDK docs and the canonical API.
Prerequisites
- Node.js (LTS recommended)
- npm or yarn
- A Chromium-based browser for full feature support (OPFS, WebRTC, optional WebGPU)
Install & run
npm install
cp .env.local.example .env.local # then fill in keys (see below)
npm run dev # http://localhost:3000Scripts
| Script | Purpose |
|---|---|
npm run dev |
Next dev server on localhost |
npm run dev:network |
Next dev server bound to 0.0.0.0 for LAN multiplayer testing |
npm run build |
Production build |
npm run start |
Run the production build |
npm run lint |
ESLint |
npm run typecheck |
tsc --noEmit |
Create a .env.local at the repo root. Variables prefixed with NEXT_PUBLIC_ are exposed to the browser; everything else is server-only.
Server-side
| Variable | Purpose |
|---|---|
ELEVENLABS_API_KEY |
ElevenLabs TTS / audio API key (used by server routes only) |
Client-side (optional PeerJS overrides)
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_PEER_HOST |
Self-hosted PeerJS broker host |
NEXT_PUBLIC_PEER_PORT |
Broker port |
NEXT_PUBLIC_PEER_PATH |
Broker path (e.g. /) |
NEXT_PUBLIC_PEER_SECURE |
true / false |
NEXT_PUBLIC_PEER_DEBUG |
PeerJS debug level 0–3 |
NEXT_PUBLIC_MULTIPLAYER_DEBUG |
Force-enable the multiplayer debug panel |
Client-side (optional cogentlm debug)
| Variable / Flag | Purpose |
|---|---|
?debugCogent=1 (URL) |
Enable verbose [CogentLoad] console logging |
localStorage.promptcastCogentDebug = "1" |
Persistent equivalent of the URL flag |
Cogentlm-specific configuration (API keys, endpoints, model overrides) — see cogentlm.com for the current SDK contract. The current integration uses the in-browser worker engine and does not require a server-side key.
Never commit .env.local or any file containing API keys.
.env.localmust remain gitignored. Verify withgit check-ignore .env.localbefore committing.- Treat any key that lands in a tracked file (or in your shell history, screenshots, or chat logs) as leaked and rotate it immediately at the provider.
- Server-only secrets must not be prefixed with
NEXT_PUBLIC_— that prefix ships them to the browser. - ElevenLabs and any future provider keys belong only in environment variables consumed by server routes / serverless functions.
- Audit
git log -p -- .env*periodically to catch accidental commits.
src/
app/ # Next.js App Router
page.tsx # Lobby (sigil inscription / threshold)
game/ # Match route
dev/ # Developer tool routes (see below)
layout.tsx, globals.css
components/game/ # React 3D + UI components
game/
ai/
cogentSpellGenerator.ts # cogentlm engine + model loader + progress
pipeline/ # concept → balance call pipeline
grammar/ # JSON grammars constraining model output
spellLog.ts # stage-tagged pipeline logger
spellGenerationError.ts # typed errors with `canRepair`
arena/ # Arena scene + props
audio/ # Audio engine, ElevenLabs hooks
config/ # Tunables
math/ # Vector / curve helpers
networking/
peerSession.ts # PeerJS host/client session, auth, debug
usePeerSession.ts # React bindings
messages.ts # Network message types + parser
physics/ # Rapier wrappers
spells/
spellSchema.ts # Zod schema for generated spells
runtime/ # Spell execution engine
modules/ # Reusable spell building blocks
vfx/ # VFX module library
sceneNode.ts # Scene graph helpers
balance.ts # Balance pass utilities
state/ # Zustand stores
playerProfile.ts # Local player identity
types.ts # Shared types
public/
audio/, textures/, models # Static game assets
PromptCast multiplayer is peer-to-peer over WebRTC using PeerJS. The host's PeerJS id is derived deterministically from the room code (promptcast-<lowercase-code>), so a sigil string is the address — clients connect by name, no signaling lookup needed beyond the public PeerJS broker (or your own — see env vars). The host is authoritative for the player list and room state; clients send intents, host validates and rebroadcasts. See src/game/networking/peerSession.ts:81 for the session implementation.
- Run
npm run devfor same-machine browser testing, ornpm run dev:networkto expose the Next dev server on the local network. - Open
/in browser A and click Inscribe Sigil. - Copy the generated sigil.
- Open
/in browser B or an incognito window, enter the sigil, and click Cross the Threshold. - The host can click Begin the Duel; clients auto-route on match start or can click Step Through once data-ready.
The multiplayer debug panel is available in development builds and with ?debugNetwork=1. It shows PeerJS state, lobby/store player ids, message events, drops, and debug actions.
Optional PeerJS client config:
NEXT_PUBLIC_PEER_HOST=localhost
NEXT_PUBLIC_PEER_PORT=9000
NEXT_PUBLIC_PEER_PATH=/
NEXT_PUBLIC_PEER_SECURE=false
NEXT_PUBLIC_PEER_DEBUG=1
NEXT_PUBLIC_MULTIPLAYER_DEBUG=1
Direct /game?mode=host&room=... URLs only set local game mode; they do not create or rejoin PeerJS sessions. Start from the lobby for real multiplayer sessions.
| Symptom | Likely cause |
|---|---|
| Client times out connecting | Host's PeerJS id never opened; check broker reachability, NAT/firewall, or NEXT_PUBLIC_PEER_* overrides |
actor_mismatch drops in debug panel |
Client tried to send a message claiming another player's id; check authorization in peerSession.ts:401 |
client_sent_host_authority_message drops |
A client attempted to send player_list, room_state, pickup_state, etc. — those are host-only |
invalid_payload drops |
Message failed parseNetworkMessage — bump the schema in src/game/networking/messages.ts |
| Host connection closed before opening | Connection settled before the data channel opened; check console for PeerJS error |
| Where | What it does |
|---|---|
?debugNetwork=1 or dev build |
Multiplayer debug panel: PeerJS state, lobby/store player ids, message in/out/drop events, debug actions |
?debugCogent=1 or localStorage.promptcastCogentDebug = "1" |
Verbose [CogentLoad] console logs covering engine create, OPFS asset registration, and model load progress |
window.__promptcastCogentDiagnostics |
Last ~80 cogent diagnostic entries (also persisted to localStorage.promptcastCogentDiagnostics) |
spellLog (src/game/ai/spellLog.ts) |
Stage-tagged pipeline log — inspect concept/balance call traces and failure causes |
Mounted under src/app/dev/:
| Route | Purpose |
|---|---|
/dev/audio-library |
Browse and audition audio assets used by spells |
/dev/vfx-preview |
Live VFX module preview harness |
/dev/vfx-textures |
Inspect VFX textures (also see public/textures/vfx/README.md) |
- Pipeline failure? Open the console, look for
[spellLog]/[CogentLoad]lines, and inspect theSpellGenerationError'sstage+cause. The UI offers a Repair retry whencanRepairis true. - Model won't load? Set
?debugCogent=1, hard-reload, and read theengine:create:*andmodel:load:*events. Check OPFS quota and that the page is in a secure context. - Networking issue? Open
?debugNetwork=1, watch the live event feed, and cross-reference dropped messages against the table above. - Visuals look wrong? Use
/dev/vfx-previewto isolate a module away from the full match.
npm run build
npm run startProduction deployment requires:
ELEVENLABS_API_KEYset as a server-side secret- Hosting that supports Next.js App Router (Vercel works out of the box)
- A reachable PeerJS broker — the public broker works for casual testing; production should self-host (set
NEXT_PUBLIC_PEER_*)
npm run lintandnpm run typecheckmust pass before you push.- Keep cogentlm pipeline changes localized to
src/game/ai/; runtime changes localized tosrc/game/spells/runtime/. Cross-cutting changes deserve a design note. - Don't widen the network message schema without updating both
messages.tsand the host authority list inpeerSession.ts:authorizeMessage. - Never commit secrets — see Secrets & Security.
- Project / SDK site: https://www.cogentlm.com
cogentlmpackage: https://www.cogentlm.com- Next.js: https://nextjs.org
- React Three Fiber: https://r3f.docs.pmnd.rs
- Rapier: https://rapier.rs
- PeerJS: https://peerjs.com