# 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<string, string>;
}

interface CharacterHarness {
  fromPhoto(image: Buffer | string, options?: Img2MeshRequest): Promise<{
    mesh: RigResult;
    defaultPose?: AnimationClipResult;
  }>;
  generateAnimation(from: "pose" | "video" | "procedural", input: any): Promise<AnimationClipResult>;
}
```

---

## 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.*
