# Camera pack — third-person follow / orbit (V1)

Default framing layer of the substrate medium. AI constructs framing through this pack so movement feel can be judged honestly — bad framing must not be misdiagnosed as bad locomotion.

```
Movement pack  →  target feet / body position
Camera pack    →  orbit yaw/pitch · lag · FOV · shoulder
DevTune        →  owned dials · Keep framing inputs
```

## Purpose

- One owner of **camera pose output** (this pack)
- Consume target position from movement (or any `getTargetPosition`)
- Own framing **inputs** via `thing('camera.*', { … })`
- Live dials change framing immediately where honest
- Keep writes inputs, never transient matrices

## AI-facing API

```js
import { createCameraPack } from '../../../camera/pack.js';

const camTune = thing('camera.player', {
  distance: 6,
  height: 2.2,
  shoulderOffset: 0.55,
  fov: 55,
  positionLag: 0.12,
  pitchMin: -35,
  pitchMax: 65,
  lookSensitivity: 0.0035
});

const cams = createCameraPack({
  thing,
  getTargetPosition: () => movement.getPosition('player'),
  camera // THREE.PerspectiveCamera
});

cams.createFollowCamera('player', { tune: camTune });

// look
cams.setLookInput('player', { dx, dy }); // pixels or normalized deltas

// ⭐ WASD — always via the pack (do not hand-roll sin/cos; AI must not re-derive this)
const { x, z } = cams.getPlanarIntent('player', { forward: wMinusS, strafe: dMinusA });
movement.setInput('player', { x, z, jump });

// after movement.step(dt):
cams.step(dt);
```

Public surface (frozen names):

| API | Role |
|---|---|
| `createCameraPack({ thing, getTargetPosition, camera })` | Bind pack to three camera + target |
| `createFollowCamera(id, { tune })` | Register orbit/follow with owned tune |
| `setLookInput(id, { dx, dy })` | Yaw/pitch intent for this frame |
| `setYaw(id, yaw)` / `getYaw(id)` | Optional for movement-relative input |
| `step(dt)` | Integrate lag + apply pose + FOV |
| `destroy()` | Clear |

## Owned keys (V1)

| Key | Meaning |
|---|---|
| `distance` | Orbit radius from target (m) |
| `height` | Vertical offset of look pivot / orbit height bias (m) |
| `shoulderOffset` | Lateral offset along camera right (m) |
| `fov` | Vertical FOV degrees |
| `positionLag` | Follow smoothing time constant (s); 0 = snap |
| `pitchMin` / `pitchMax` | Pitch clamp degrees |
| `lookSensitivity` | Radians (or rad/pixel scale) applied to look deltas |

No invented ranges — rangeless steppers unless the game authors them.

## Live vs careful

| Params | Policy |
|---|---|
| distance, height, shoulderOffset, fov, lookSensitivity, pitch limits | **Live** every step |
| positionLag | **Live** (0 = hard follow; higher = softer) |

Collision pull-in: **not V1** (residual).

## Integration with movement

- Target = feet (or body) from `movement.getPosition(id)`  
- Camera **does not** write character transform  
- Movement **does not** write camera transform  
- Game may use `cams.getYaw(id)` for camera-relative WASD  

## Keep rules

- Literals at `thing('camera.player', { … })` call site  
- Never Keep camera.matrix / quaternion  
- Kernel (`planEdits`) is the only writer  

## Non-goals

Cinematic director · timelines · lock-on graphs · multi-VCam mixer · FPS as default · rewrite physics/movement packs  

## Proof

Integrated into `substrate/gen/movement-proof/` (preferred) and documented in `CAMERA_REPORT.md`.

```bash
node adapters/serve/serve.mjs substrate --port 5430 --shell
# http://127.0.0.1:5430/gen/movement-proof/
```
