# Third-Person Camera

## Metadata

| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M02 / M02-RND-CAMERA-001 and M02-GMP-INPUT-PROFILE-335-001 consumer update |
| Owners | Camera presentation state and CameraPivot/Camera3D lifecycle |
| Last verified | M02-GMP-INPUT-PROFILE-335-001 worktree, 2026-07-15 |
| Profiles/capabilities | Current sandbox orbit/zoom; identity collision policy |

## Purpose

Own third-person mouse capture, orbit yaw/pitch, zoom state and Camera3D local
transforms on the existing `CameraPivot` node. Expose an explicit replaceable
camera-distance collision policy while preserving the current no-collision
sandbox baseline.

## Non-goals

- Implement an unverified physics raycast or claim build-12340 collision parity.
- Own player movement, terrain queries, streaming focus or character visuals.
- Implement first-person, follow lag, shoulder camera, cinematics or view modes.
- Change current sensitivity, pitch/zoom limits, FOV or far plane.

## Context and boundaries

```mermaid
flowchart LR
    InputMap[Remappable camera actions] --> Player[ThirdPersonWowController dispatch]
    Player --> Rig[ThirdPersonCameraRig]
    Mouse[Mouse motion] --> Player
    Rig --> Character[Character yaw transform]
    Rig --> Pivot[CameraPivot local transform]
    Rig --> Policy[CameraCollisionPolicy]
    Policy --> Distance[Resolved distance]
    Distance --> Camera[Camera3D local transform]
    Rig --> FlightBasis[Godot-world pivot basis]
    FlightBasis --> Movement[LocalPlayerMovementController adapter]
```

Allowed dependencies:

- Existing `PlayerInputActions` names and Godot input events.
- Explicit character, pivot and Camera3D Node references on the main thread.
- Replaceable `CameraCollisionPolicy` selected by composition/tests.
- Movement adapter may read the rig's Godot-world basis during sandbox flight.

Forbidden dependencies:

- ADT/terrain parsing, movement velocity, gameplay state or animation ownership.
- Camera rig discovering renderer/network/server services.
- Default collision policy performing hidden raycasts or changing baseline distance.
- Collision policy mutating rig/camera/character nodes.

## Public API

| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `ThirdPersonCameraRig` | `Node3D` component | Owns orbit/zoom state on CameraPivot | Main thread; scene-owned | Reports missing Camera3D |
| `initialize_for_character(character_body)` | Composition method | Attaches yaw target and captures initial character yaw | Main thread after scene ready | Null leaves yaw target absent |
| `synchronize_yaw_from_character()` | Composition method | Refreshes orbit yaw after keyboard movement turns the character | Main physics thread | Missing character leaves yaw unchanged |
| `handle_camera_input(event)` | Input adapter | Applies capture/release/zoom/orbit actions | Main input thread | Returns whether camera consumed state |
| `set_collision_policy(policy)` | Composition method | Replaces distance resolution policy | Main thread | Null rejected; old policy retained |
| `refresh_camera_transform()` | Presentation update | Applies pivot pose and resolved camera distance | Main/physics thread | Missing camera leaves state but no Camera3D mutation |
| `godot_world_flight_movement_basis()` | Read model | Returns pivot global basis for sandbox flight | Main thread | None |
| `CameraCollisionPolicy.resolve_camera_distance(pivot, requested)` | Policy boundary | Resolves camera distance in Godot units | Main thread in current implementation | Rig clamps output to `[0, requested]` |
| `NoCameraCollisionPolicy` | Identity policy | Preserves requested distance with no physics query | Any main-thread call | None |

Read-only rig state: `yaw_radians`, `pitch_radians`,
`requested_distance_units`, `resolved_distance_units` and `is_mouse_captured`.

## Inputs and outputs

| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Camera action `InputEvent` | Godot/player dispatch | Camera rig | Engine-owned event | One dispatch |
| Input | Mouse relative pixels | Godot input | Camera rig | Engine-owned event | One dispatch |
| Input | Character `Node3D` | Player composition | Camera rig | Scene-owned reference | Rig scene lifetime |
| Input | Keyboard-turn synchronization | Player scene after yaw application | Camera rig | Synchronous notification | Current physics tick |
| Input | Collision policy | Composition/test | Camera rig | Rig-held RefCounted | Until replacement |
| Output | Character yaw | Camera rig | Player transform | Scene mutation | Orbit event |
| Output | Pivot pitch/height | Camera rig | CameraPivot | Scene mutation | Refresh |
| Output | Requested/resolved distance | Camera rig/policy | Camera3D | Rig-owned scalar/read model | Until next input/refresh |
| Output | Godot-world `Basis` | Camera rig | Flight movement adapter | Value copy | Physics tick |

Side effects:

- Mutates mouse mode, character yaw, CameraPivot local transform and Camera3D local transform.
- Marks Camera3D current and sets its far plane once at ready.
- Releases captured mouse on rig removal.
- Creates no files, caches, RIDs, workers, network messages or gameplay state.

## Data flow

```mermaid
flowchart LR
    Action[Camera action or mouse delta] --> State[Yaw pitch requested distance capture]
    State --> Clamp[Pitch/zoom clamps]
    Clamp --> Pivot[Pivot height and pitch]
    Clamp --> Request[Requested distance]
    Request --> Policy[CameraCollisionPolicy]
    Policy --> Safe[Clamp 0..requested]
    Safe --> Camera[Camera3D Z distance]
    State --> Character[Character yaw]
```

## Lifecycle/state

```mermaid
stateDiagram-v2
    [*] --> MouseVisible
    MouseVisible --> MouseCaptured: camera rotate pressed
    MouseCaptured --> MouseVisible: camera rotate released
    MouseCaptured --> MouseVisible: release cursor
    MouseCaptured --> MouseCaptured: mouse motion / orbit and clamp
    MouseVisible --> MouseVisible: zoom and clamp
    MouseCaptured --> [*]: rig removed / release mouse
    MouseVisible --> [*]: rig removed
```

Zoom and collision distance are orthogonal to capture state. The requested zoom
persists while a policy may temporarily choose a shorter resolved distance.

## Main sequence

```mermaid
sequenceDiagram
    participant Engine as Godot Input
    participant Player as ThirdPersonWowController
    participant Rig as ThirdPersonCameraRig
    participant Policy as CameraCollisionPolicy
    participant Camera as Camera3D
    Engine->>Player: InputEvent
    Player->>Rig: handle_camera_input(event)
    Rig->>Rig: update capture/orbit/zoom state
    Rig->>Policy: resolve_camera_distance(pivot, requested)
    Policy-->>Rig: resolved distance
    Rig->>Camera: apply local position/rotation
    opt captured mouse motion
        Rig->>Player: mutate character yaw transform
    end
```

## Ownership, threading and resources

- Parent player scene owns the CameraPivot rig and Camera3D child.
- Rig holds non-owning scene references to character and camera.
- Rig owns scalar camera state and the RefCounted collision policy.
- All input/state/scene mutations occur on the main thread.
- Policy output is a scalar; policies must not mutate supplied nodes.
- No background tasks, resources, RIDs or caches are created.

## Errors, cancellation and recovery

| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Camera3D missing | `_ready` lookup | State continues; transform application skipped | `ThirdPersonCameraRig` error with path | Restore child/path and recreate scene |
| Character missing | Initialization | Orbit state updates without character yaw mutation | Composition/test failure | Call initializer with player node |
| Null collision policy | Setter guard | Current policy retained | Rig error | Pass valid policy |
| Policy returns negative/too large | Rig clamp | Distance clamped to `[0, requested]` | Policy regression tests | Fix/replace policy |
| Rig removed while captured | `_exit_tree` | Mouse made visible | Lifecycle regression | Recreate rig normally |

There is no asynchronous cancellation or persisted rollback. Recreating the rig
restores configured defaults.

## Configuration and capabilities

| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Initial pitch | `-18°` | Current sandbox | State-owned after ready | Initial orbit elevation |
| Pitch limits | `-65°..35°` | Current sandbox | Exported | Mouse clamp |
| Mouse sensitivity | `0.003` rad/pixel | Current sandbox | Exported | Orbit delta |
| Pivot height | `1.7` units | Current sandbox | Exported | Camera eye origin |
| Initial distance | `8` units | Current sandbox | Exported before ready | Requested zoom |
| Zoom limits/step | `2..18`, step `1` | Current sandbox | Exported | Requested distance clamp |
| Far plane | `50000` units | Renderer sandbox | Exported | Camera far plane |
| Collision policy | `NoCameraCollisionPolicy` | Current sandbox | Replaceable | Identity distance/no raycast |

## Persistence, cache and migration

Camera state and policy are transient. No SavedVariables/settings schema, cache
or migration is introduced. Scene paths remain
`ThirdPersonPlayer/CameraPivot/Camera3D`, so renderer diagnostics and capture
tools require no path migration.

## Diagnostics and observability

- Logs: missing camera/null policy emit explicit component errors.
- Read models: yaw, pitch, requested/resolved distance and capture state.
- Tests: dedicated verifier reports state/policy/scene counts.
- Metrics/correlation IDs: not applicable to constant-time local presentation.

## Verification

- State regression: initial yaw/pitch/distance/height/far plane, capture/release,
  mouse orbit, character yaw, pitch clamps and zoom clamps.
- Input-profile integration verifies keyboard yaw synchronization, preventing a
  later right-mouse orbit from restoring stale character yaw.
- Policy regression: fixed-distance test policy changes Camera3D Z; identity
  policy restores requested distance; baseline policy contains no physics query.
- Scene integration: Eastern Kingdoms, Kalimdor and asset-free regression scenes
  attach the rig to the existing CameraPivot.
- Boundary: player no longer owns capture/yaw/pitch/distance or camera transform helper.
- Existing input/movement/terrain regressions and renderer dry-run remain required.
- Fidelity evidence: current numeric defaults and identity collision behavior
  only; original-client orbit/collision comparison remains open.
- Performance: constant-time scalar/transform work once per physics tick and
  input event; no default raycast.

## Extension points

- Add an opt-in raycast collision policy after reference fixtures define mask,
  margin, recovery smoothing and first-person limit.
- Add follow/shoulder/cinematic rigs as separate components, not branches inside this rig.
- Persist user camera settings through a future versioned settings module.
- Input contexts may suppress camera actions before dispatch without changing rig state.

## Capability status

| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Independent third-person rig | Implemented | Three scene wirings and player boundary regression | Integrate target checklist after review |
| Existing orbit/zoom behavior | Implemented | Numeric state/transform regression | Compare against build 12340 |
| Camera-relative sandbox flight basis | Implemented | Basis and movement regressions | Restrict flight to sandbox profile |
| Replaceable collision policy | Implemented | Fixed and identity policy tests | Implement verified collision policy |
| Active camera collision | Planned | Identity policy intentionally preserves baseline | Capture original-client occluder fixtures first |
| First-person/follow policies | Planned | Outside current sandbox baseline | Later completeness work |

## Known gaps and risks

- Default collision policy performs no occluder avoidance, matching the previous sandbox.
- Exact WoW camera orbit direction, pitch limits, zoom curve, collision margin and
  recovery smoothing are unverified.
- Exported initial values are captured at ready; runtime mutation may require an
  explicit reconfiguration API later.
- Mouse mode remains process-global Godot state and must be coordinated with future UI contexts.

## Source map

| Path | Responsibility |
|---|---|
| `src/scenes/player/third_person_camera_rig.gd` | CameraPivot state, input and transforms |
| `src/scenes/player/camera_collision_policy.gd` | Replaceable distance-policy boundary |
| `src/scenes/player/no_camera_collision_policy.gd` | Identity baseline policy |
| `src/scenes/player/third_person_wow_controller.gd` | Camera event dispatch and movement-basis consumer |
| `src/scenes/streaming/*_streaming.tscn` | Runtime rig composition |
| `src/tests/scenes/player_input_regression.tscn` | Asset-free rig/player composition |
| `src/tools/verify_third_person_camera_rig.gd` | State, policy, scene and boundary regression |

## Related decisions and references

- ADR: none; current behavior and scene paths are preserved.
- Upstream/reference: `targets/02-player-decomposition.md`,
  `targets/roadmap/02-rendering-and-graphics.md`,
  `targets/roadmap/04-gameplay-systems.md`, `docs/ARCHITECTURE.md`.
