# PSX Adventure Engine **Reference Project / Game Design Document** Version 0.1 — July 2026 --- ## 1. Vision A modern, browser-first **reference engine and project template** for creating authentic PSX-era third-person survival-horror and action-adventure games in the style of: - Resident Evil 1 & 2 - Parasite Eve - Dino Crisis - Silent Hill (classic) The engine prioritizes: - Fixed or limited cameras - Tank or camera-relative controls - Inventory, item combination, and puzzle gameplay - Atmospheric low-poly aesthetics - True peer-to-peer multiplayer - Rapid content creation via photo-to-character pipelines It is designed as a clean, fully-typed foundation that teams can fork and extend into complete games (or other genres later). --- ## 2. Core Pillars 1. **Authentic PSX Feel** Low-poly geometry, affine texture mapping, dithering, color quantization, fixed cameras, optional tank controls, atmospheric fog and limited draw distance. 2. **Content Velocity** Powerful img2mesh + common-skeleton harness so characters and items can be generated from reference photographs and immediately animated. 3. **Multiplayer-Native** Host-authoritative P2P WebRTC for 2–4 player co-op out of the box. 4. **Cloud Persistence** Neon-backed accounts, cloud saves, lobby system, and shared campaign progress. 5. **Clean Architecture** Strict separation between runtime engine, backend services, and content-creation harnesses. --- ## 3. Goals & Non-Goals ### Goals - Provide a production-quality reference "engine of sorts" for the target genre. - Support both classic fixed-camera and freer camera styles. - Deliver true browser P2P multiplayer with host-authoritative simulation. - Enable rapid character and item creation from reference photos. - Keep everything TypeScript-first and highly modular. ### Non-Goals - Competing with Unity or Unreal as a general-purpose engine. - High-fidelity modern PBR graphics or complex soft-body physics. - Dedicated-server authoritative multiplayer (we stay pure P2P + lightweight signaling). - Mobile-first design (desktop + modern browsers are the primary target). --- ## 4. Technical Stack | Layer | Technology | |--------------------|-------------------------------------------------| | Runtime | Three.js (vanilla or React Three Fiber) + TypeScript + Vite | | Physics | Box3D (via `box3d-wasm`) | | Networking | PeerJS (or simple-peer) + custom WebSocket signaling | | Backend | Hono + Neon Postgres | | Asset Pipeline | Node-based tools + optional local web UI | | Build / Monorepo | pnpm / Turborepo or simple Vite workspaces | --- ## 5. Monorepo Structure ``` / ├── client/ # Three.js game runtime │ ├── src/ │ │ ├── engine/ # core loop, scene manager │ │ ├── systems/ # Camera, Player, Inventory, Interaction, Multiplayer… │ │ ├── post/ # PSX visual pipeline │ │ └── ... │ └── public/ │ ├── server/ # Hono API + WebSocket signaling │ ├── routes/ # auth, saves, lobbies │ ├── ws/ # SDP/ICE exchange │ └── db/ # Neon + Drizzle/Prisma │ ├── shared/ # Shared types & definitions │ ├── skeleton/ # canonical skeleton │ ├── types/ │ └── schemas/ │ ├── tools/ # Content creation harness │ ├── src/ │ │ ├── img2mesh/ │ │ ├── rig/ │ │ ├── anim/ │ │ ├── items/ │ │ ├── postprocess/ │ │ └── export/ │ └── web/ # optional local UI │ └── assets/ # Generated GLBs + animation clips ``` --- ## 6. Core Runtime Systems ### 6.1 CameraManager - Fixed camera zones using trigger volumes (classic RE style). - Smooth or hard cuts between cameras. - Optional free / follow / rail cameras for Silent Hill style. - Camera-relative movement so "up" always feels correct after a cut. ### 6.2 PlayerController - Classic tank controls (toggleable). - Modern camera-relative movement. - Quick-turn, aim, interact, item use. - Uses Box3D character mover under the hood. ### 6.3 InventorySystem - Classic RE-style list or RE4-style grid. - Item examination, combination, and key-item usage. - Fully serializable to JSON for cloud saves. ### 6.4 Interaction & Puzzle Framework - Examine, pick up, use item on object, door/key systems. - Trigger volumes and state machines for puzzles. - Sensors in Box3D for detection. ### 6.5 Combat & AI (baseline) - Simple melee and limited-ammo ranged combat. - Basic enemy behaviors suitable for the genre. - Host simulates all AI and damage. ### 6.6 Multiplayer Layer See section 8. --- ## 7. Physics — Box3D Box3D (Erin Catto, 2026) is the primary physics engine. **Usage guidelines:** - Player → Capsule + character mover - Rooms / static geometry → Triangle meshes or static compounds - Triggers (cameras, items, doors, puzzles) → Sensors - Dynamic props / enemies → Capsules or convex hulls - Determinism is leveraged for host-authoritative multiplayer Integration via `box3d-wasm`. Colliders and sensors are auto-generated or defined in asset metadata. --- ## 8. Multiplayer Design - **Transport**: PeerJS (or simple-peer) data channels after WebSocket signaling. - **Model**: Host-authoritative. - Host (usually lobby creator) owns world simulation, AI, inventory truth, door/puzzle states. - Clients send high-level actions / inputs. - Host validates and broadcasts state or deltas. - Update rate: 10–20 Hz (adventure pacing is forgiving). - Interpolation + simple prediction on clients. - STUN (Google public) by default; TURN optional for reliability. - Shared campaign progress is written to Neon on checkpoints or session end. --- ## 9. Backend & Persistence (Neon) ### Core Tables ```sql users ( id UUID PRIMARY KEY, username TEXT UNIQUE, email TEXT UNIQUE, display_name TEXT, created_at, last_login ) saves ( id UUID PRIMARY KEY, user_id UUID REFERENCES users, slot INT, campaign TEXT, title TEXT, data JSONB NOT NULL, -- inventory, flags, positions, puzzle states… playtime_seconds INT, updated_at, UNIQUE(user_id, slot, campaign) ) lobbies ( id UUID PRIMARY KEY, code TEXT UNIQUE, -- short room code host_id UUID, status TEXT, -- waiting / starting / in_game / closed max_players INT DEFAULT 4, settings JSONB, created_at, expires_at ) lobby_players ( lobby_id, user_id, peer_id INT, is_ready BOOLEAN, PRIMARY KEY (lobby_id, user_id) ) ``` JSONB is used heavily for flexible save data and lobby settings. --- ## 10. Asset Pipeline & Harness ### 10.1 Prioritization 1. **Primary (Characters) — shipped path** Photo (Grok `/imagine` or file) → silhouette measure → code-drawn low-poly mesh on the canonical skeleton → automatic skin weights → procedural clips → GLB. Geometry is constructed in code from measured proportions; we do **not** use Meshy or other third-party mesh APIs for the default harness. 2. **Secondary (Props / Stylized)** Same measurement → code-drawn approach, simplified for non-humanoids. ### 10.2 Canonical Skeleton Mixamo-compatible core set: ``` Hips Spine → Spine1 → Spine2 → Neck → Head LeftShoulder → LeftArm → LeftForeArm → LeftHand RightShoulder → RightArm → RightForeArm → RightHand LeftUpLeg → LeftLeg → LeftFoot → LeftToeBase RightUpLeg → RightLeg → RightFoot → RightToeBase ``` Defined in `shared/skeleton/canonical.ts`. All auto-riggers map into these names. ### 10.3 Tools Folder Structure ``` tools/ ├── src/ │ ├── grok/ # Grok CLI bridge (image_gen / image_edit) │ ├── image/ # decode, segment, measure reference photos │ ├── mesh/ # code-drawn humanoid from measurements │ ├── rig/ # canonical skeleton + automatic skin weights │ ├── anim/ # procedural Idle / Walk / Run / QuickTurn │ ├── export/ # hand-rolled GLB writer │ ├── pipeline.ts # end-to-end photo → GLB │ └── cli.ts # `pnpm harness` └── fixtures/ # reference images for tests ``` ### 10.4 Key Interfaces (TypeScript) ```ts interface Img2MeshRequest { image: Buffer | string; style?: "realistic" | "lowpoly" | "psx"; provider?: "meshy" | "triposr" | "img2threejs"; } interface RigResult { glbPath: string; boneMap: Record; } interface CharacterHarness { fromPhoto(image: Buffer | string, options?: Img2MeshRequest): Promise<{ mesh: RigResult; defaultPose?: AnimationClipResult; }>; generateAnimation(from: "pose" | "video" | "procedural", input: any): Promise; } ``` --- ## 11. PSX Visual Pipeline - Vertex snapping - Affine texture mapping - Ordered dithering - Color depth reduction - Low-resolution render target + nearest-neighbor upscale - Optional CRT / scanlines / fog - Implemented via material `onBeforeCompile` patches + EffectComposer passes --- ## 12. Demo Content (First Playable) **Ashgrove Precinct — Level 1**: a procedural crime-investigation loop inside a classic fixed-camera survival-horror building. Traverse rooms, question living NPCs, gather evidence, and open locked wings with inventory puzzles. - 9 connected rooms with fixed cameras (lobby, squad hall, office, interview, lounge, corridor, records, break room, evidence vault) - One dedicated room per ambient cast member + player Detective body - Question / examine dialogue on every NPC (Reyes, Ellis, Voss, Kane, Hale, Marrow) - Inventory + key items + crank/valve + brass/vault key progression - Evidence pickups (statement, badge case, photograph, journal) - 2-player co-op lobby - Cloud save / load - Harness-driven cast GLBs with Idle / Walk / Slumped poses --- ## 13. Roadmap / Milestones **Milestone 1 – Core Runtime** - CameraManager + PlayerController (tank + camera-relative) - Basic room system + Box3D integration - Inventory + Interaction **Milestone 2 – Multiplayer** - PeerJS lobby + host-authoritative movement & interactions - Neon auth + cloud saves **Milestone 3 – Asset Harness** - Grok CLI bridge for reference generation (`image_gen`) - Silhouette analysis → measurements + palette - Code-drawn mesh on canonical skeleton + automatic skin weights - Procedural locomotion clips + GLB export into the runtime **Milestone 4 – First Playable Demo** - Complete small campaign - Full 2-player co-op - Documentation & example workflows **Later** - Expanded animation library - More advanced AI & combat - Additional camera modes - Modding / user-generated content hooks --- ## 14. Style Guide (PSX Aesthetic Rules) - Prefer low polygon counts and limited texture resolution. - Embrace affine distortion and vertex jitter where it serves atmosphere. - Use strong silhouettes and readable shapes over fine detail. - Fog, limited draw distance, and carefully placed lights are primary tools for tension. - UI should feel period-appropriate (simple, slightly chunky, limited color palette). --- ## Appendix A — Canonical Skeleton Definition Full TypeScript definition lives in `shared/skeleton/canonical.ts` (see section 10.2). ## Appendix B — Neon Schema Full SQL is maintained in `server/db/schema.sql` (see section 9). ## Appendix C — Key Interfaces See section 10.4 and the `tools/src/**/types.ts` files. --- ## Appendix D — Implementation Addendum (Milestone 1, July 2026) Recorded as systems were implemented, per the note below that this is a living document. Nothing here changes the design intent; it records where the intent met the current state of the dependencies. ### D.1 Box3D capability gaps `box3d-wasm@0.2.0` (published 2026-07-02) binds less of upstream Box3D than §6.2 and §7 assume. Verified by enumerating the embind prototypes on the built wasm module: | Assumed in this document | Actual | Resolution | | --- | --- | --- | | Box3D character mover (§6.2, §7) | Not bound | Hand-rolled collide-and-slide capsule mover, `client/src/systems/CharacterMover.ts` | | Triangle-mesh room collision (§7) | No trimesh shape; only box, sphere, capsule, hull | Rooms are static compounds of boxes and convex hulls. `ColliderDef` in `shared` is deliberately narrowed to the four supported primitives so the asset pipeline cannot emit collision the runtime can't build | | Capsule sweeps for the mover | No shape cast or overlap query; `castRayClosest` and `shape.rayCast` only | A fan of ray probes (three heights × three lateral offsets) approximates the sweep | Confirmed working and load-bearing: raycasts with filters, sensor begin/end events keyed by `userData`, kinematic bodies via `setTargetTransform`, convex hulls, contact and body events. The package ships no TypeScript declarations. `client/src/types/box3d-wasm.d.ts` declares the API as observed at runtime rather than as documented — the README describes a slightly larger surface than is actually bound. ### D.2 Determinism claim in §7 §7 states determinism is leveraged for host-authoritative multiplayer. These are independent properties: host-authoritative simulation requires only that one peer owns the state and broadcasts it, which is what §8 actually describes. Determinism is a requirement of *lockstep* networking, which this project is not using. Milestone 2 should not be designed around a determinism guarantee that a 0.2.0 wasm build has not demonstrated. ### D.3 Userdata tag collisions box3d-wasm keeps separate auto-increment counters for body and shape `userData` tags, so an auto-assigned body tag and shape tag can be numerically equal. Since sensor and contact events reference *shape* tags, `PhysicsWorld` assigns every tag from a single counter of its own rather than relying on the auto-assigned values. ### D.4 Vite worker format The threaded ("deluxe") physics build spawns emscripten pthread workers whose entry uses top-level await. Vite bundles workers as `iife` by default, which cannot express that, and the production build fails. `worker: { format: "es" }` in `client/vite.config.ts` is required, not optional. ### D.5 Affine texture mapping technique GLSL ES 3.00 has no `noperspective` interpolation qualifier, so §11's affine mapping cannot be requested directly. It is instead produced arithmetically: the vertex shader emits `uv * w` and `w` as varyings and the fragment shader divides them. The interpolator's own `1/w` factors cancel, leaving screen-linear UVs. The patch is applied only to materials that carry a `map`, since without one three.js may not declare the `uv` attribute at all. ### D.6 WebRTC transport: neither PeerJS nor simple-peer §4 and §8 name "PeerJS (or simple-peer) + custom WebSocket signaling". Those two halves are in tension, and neither library fits: - **simple-peer** depends on `buffer` and `readable-stream`, Node polyfills that are a recurring source of Vite build breakage. - **PeerJS** is built around its own broker protocol. Using it would *replace* the custom WebSocket signalling §4 calls for, not complement it. What both libraries would have provided is a data channel following an SDP exchange, which is a thin wrapper over `RTCPeerConnection`. `client/src/net/ PeerLink.ts` is that wrapper, with no dependencies, and it matches §8's description exactly: data channels established after WebSocket signalling. Each link carries **two** channels rather than one, because gameplay traffic has two delivery profiles: `state` (unreliable, unordered) for position snapshots, which are worthless once superseded, and `event` (reliable, ordered) for flags, despawns and inventory, where a single drop desynchronises the world. ### D.7 Co-op inventory is shared, not per-player §6.3 and §8 do not say whether each peer carries their own inventory. In this implementation the party shares one, held by the host. It matches how the genre's co-op modes work, and it keeps a single authoritative copy rather than four that can disagree. Guests display a read-only mirror pushed by the host. ### D.8 Host reach checks for remote interactions Interaction reach is detected with Box3D sensors, but sensors only ever track the *local* player's capsule. When a guest asks to interact, the host therefore re-derives proximity from that guest's authoritative mover position before running the interaction (`Game.handleGuestInteract`). Without this a guest could open any door in the room by naming its id. ### D.9 Key items and inventory capacity §6.3 does not say whether key items consume inventory slots. They do not, in this implementation: a full inventory would otherwise be able to soft-lock a puzzle. They are held outside the slot array and serialised separately in `InventoryState.keyItems`. --- *This document is the living design and technical specification for the PSX Adventure Engine reference project. It will be updated as systems are implemented and lessons are learned.*