Files
psxbase/GDD.md
T
ryanfitzpatrickio 8a96ede9f2 Initial public release of PSX Adventure Engine
Browser reference stack for PSX-era third-person adventure: fixed cameras,
inventory puzzles, Box3D physics, host-authoritative P2P co-op, and a modular
character harness. Ships the Ashgrove Precinct Level 1 investigation demo with
a full cast and nine linked rooms.
2026-07-31 06:32:43 -05:00

17 KiB
Raw Blame History

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 24 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: 1020 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

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)

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.