# GDD: Runner Movement Controller

**System ID:** SYS-01  
**Design Order:** 5 of 33  
**Priority:** MVP — Core Layer  
**Dependencies:** SYS-33 (FishNet Replication — prediction contract), SYS-22 (Match State Machine — freeze input at PostMatch)  
**Dependents:** SYS-04 (Weight/Burden System — modifies speed + gates abilities), SYS-05 (Geometric LoS — uses Runner's position), SYS-09 (Loot & Extraction — pickup/drop input), SYS-26 (Runner HUD — ability availability indicators), SYS-30 (Tutorial — teaches each move), SYS-32 (VFX & Juice — triggers on state transitions)  
**Status:** Designed  

---

## 1. Purpose and Priority

This system is the game. Pillar 1 states: "The Runner's skill expression lives in their movement. Every ability, item, and map layout exists to serve parkour flow." If the movement doesn't feel good in week 1, nothing else matters.

**Prototype mandate:** Build the movement controller in isolation — no FishNet, no weight system, no enemy — just a test room with obstacles. Get it feeling right before touching any other system. Then wire in weight gating (SYS-04). Then add networking.

**Feel target:** Mirror's Edge — momentum preservation through chained moves. Speed should feel earned, and losing speed should feel bad. A skilled Runner chains: sprint → slide → jump → wall jump → dash and ACCELERATES through the sequence. An unskilled Runner stumbles and loses all momentum.

---

## 2. Physics Foundation

### 2.1 CharacterController vs Rigidbody Decision

**Decision: Unity `CharacterController` with custom velocity simulation.**

Rationale:
- `CharacterController.Move()` gives precise, deterministic physics — critical for FishNet client-side prediction. Rigidbody introduces solver non-determinism that makes reconciliation unreliable.
- Manual velocity simulation means we own momentum math entirely — essential for Mirror's Edge-style momentum preservation (e.g., preserve horizontal speed through a jump, add a speed boost out of a slide).
- Wall-running and wall-jumping require tight control over when gravity applies. CharacterController makes this explicit; Rigidbody fights us here.
- Already the established pattern for FPS movement in Unity.

**CharacterController configuration:**
- `radius`: 0.3f
- `height`: 1.8f (standing), 0.9f (sliding)
- `slopeLimit`: 50°
- `stepOffset`: 0.3f
- `skinWidth`: 0.02f
- `minMoveDistance`: 0.001f

### 2.2 Custom Velocity Model

The controller maintains a `Vector3 _velocity` that is manually integrated each tick:

```
_velocity += gravity * Time.deltaTime      (when airborne, not wall-running)
_velocity += movement_force * Time.deltaTime
_velocity = clamp(_velocity, maxSpeed)
_characterController.Move(_velocity * Time.deltaTime)
```

Horizontal and vertical velocity are tracked separately so gravity doesn't kill horizontal momentum:

```csharp
private Vector3 _hVelocity;  // horizontal — controlled by movement
private float   _vVelocity;  // vertical   — controlled by gravity + jumps
```

---

## 3. Movement State Machine

The Runner has **one active movement state** at any time. States are mutually exclusive.

### 3.1 State Enum

```csharp
public enum MovementState
{
    Grounded,      // Standing/walking/sprinting on floor
    Sliding,       // Ctrl held while sprinting on floor
    Airborne,      // In air after jump or fall
    WallRunning,   // Sprint into angled wall, lateral traverse
    WallJumping,   // One frame of wall jump launch (transitions to Airborne)
    Dashing,       // Shift+direction burst (very short)
    AirDashing,    // Shift midair (horizontal only)
    Grappling,     // E on anchor point — swing arc
    Dead,          // Vaporized — controller disabled
}
```

### 3.2 State Transition Diagram

```
              ┌──────────────────────────────────────────────┐
              │                  Grounded                     │
              │  (default; CharacterController.isGrounded)    │
              └───┬───┬───┬────────────────────────────────┬─┘
                  │   │   │                                │
              Jump│   │Ctrl+Sprint  Sprint→Wall        Grapple E
                  │   │(Slide)     (WallRun)              │
                  ▼   ▼   ▼                               ▼
              ┌──────────────────┐   ┌──────┐       ┌──────────┐
              │    Airborne      │   │Sliding│      │Grappling │
              │ (gravity active) │   └───┬──┘      └──────────┘
              └──┬──┬───┬────┬──┘       │Jump           │land
                 │  │   │    │          │               │
            Land │  │ Dash Wall     ┌───▼────┐     Grounded
                 │  │   │  touch    │Airborne│
                 ▼  ▼   │  wall     └──┬──┬──┘
              Ground  AirDash  ┌───────▼  └──────────┐
                        │  WallRun   WallJumping      Dash
                        │      └──────────────────────┘
                        ▼
                   Airborne
```

### 3.3 State Entry/Exit Rules

| State | Entry Condition | Exit Condition | Weight Gate |
|-------|----------------|---------------|-------------|
| `Grounded` | `isGrounded == true` | Jump/fall | None |
| `Sliding` | Ctrl+Sprint, isGrounded, speed > slideMinSpeed | Release Ctrl, hit wall, speed < slideEndSpeed, reach Heavy | Available until Heavy |
| `Airborne` | Not grounded | Land | None |
| `WallRunning` | Sprint into wall at angle 30–80° to wall, wall height valid | Wall ends, timer expires (1.5s max), release sprint | Available until Heavy |
| `WallJumping` | Space while WallRunning or adjacent to wall, airborne | Instantly exits to Airborne | Available until Heavy |
| `Dashing` | Shift+direction, grounded, cooldown ready | Duration expires (0.2s) | Lost at Moderate |
| `AirDashing` | Shift midair, `_airDashAvailable`, cooldown ready | Duration expires (0.15s) | Lost at Light-Moderate |
| `Grappling` | E while anchor in range, item equipped | Release E, swing completes, anchor range exceeded | Item-based (grapple has own weight) |
| `Dead` | Server sets `IsAlive = false` | Scene despawn | N/A |

---

## 4. Ability Specifications

### 4.1 Sprint

| Parameter | Value |
|-----------|-------|
| Input | Hold `Shift` |
| Speed | `baseSpeed * weightTierMultiplier` |
| Acceleration | 15 m/s² (snappy, not floaty) |
| Deceleration | 20 m/s² |
| Base speed | 7.5 m/s |
| Weight gate | Always available (speed just decreases with tier) |

Sprint feeds into everything — slide, wall run, jump distance. Speed must feel satisfying at all weight tiers; even Overloaded runners should feel like they're moving with intention, just slowly.

### 4.2 Jump

| Parameter | Value |
|-----------|-------|
| Input | `Space` |
| Jump height | 2.5 m (tuned for obstacle clearance) |
| V-velocity | `Mathf.Sqrt(2 * gravity * jumpHeight)` = ~7 m/s |
| H-velocity preservation | 100% of pre-jump horizontal speed |
| Coyote time | 0.15s (still jumpable shortly after walking off edge) |
| Jump buffer | 0.1s (pre-jump input accepted slightly early) |
| Weight gate | Always available |

**Coyote time and jump buffer are mandatory.** Without them, jumps feel unreliable and the player blames themselves for the game's failure. These are the most impactful 10 lines of movement code.

**Double Jump:** Second press of `Space` while `Airborne`, before apex reached, `_doubleJumpAvailable = true`. Adds a full second jump impulse. Resets on landing. Lost at Moderate tier.

### 4.3 Slide

| Parameter | Value |
|-----------|-------|
| Input | `Ctrl` while sprinting |
| Entry condition | Speed > 4 m/s |
| Collider height | Reduces to 0.9f on entry |
| Initial impulse | +2 m/s horizontal boost (feels like diving) |
| Friction | 0.3 (slides a meaningful distance, not instant stop) |
| Minimum exit speed | 2 m/s (auto-exits if decelerated below this) |
| Duration cap | 3 seconds |
| Jump from slide | +30% horizontal speed preserved into jump |
| Weight gate | Available until Heavy tier |

**Slide-into-jump is the core combo.** Slide builds speed, jump out of it carries that speed into the air. This is the Mirror's Edge flow move that separates good runners from great ones. The +30% speed preservation from slide-jump must feel GOOD. Tune this until landing runners can clear 2-room gaps.

### 4.4 Wall Jump

| Parameter | Value |
|-----------|-------|
| Input | `Space` near wall, airborne OR entering from WallRun |
| Wall detection | SphereCast radius 0.4f, distance 0.6f from player center |
| Jump direction | Reflect velocity off wall normal, add upward component |
| Horizontal push | 4.5 m/s away from wall |
| Vertical push | 6 m/s up |
| Chain limit | 2 consecutive same-wall jumps (prevents infinite climbing) |
| Weight gate | Available until Heavy tier |

Wall jumps must feel propulsive — the player should feel like they're using the wall, not just touching it. The velocity should arc cleanly away. Test on a vertical shaft: a skilled player should be able to wall-jump up 8m in 4 bounces.

### 4.5 Dash

| Parameter | Value |
|-----------|-------|
| Input | `Shift + WASD direction` (grounded) |
| Duration | 0.2 seconds |
| Speed | 12 m/s in dash direction |
| I-frames | Full during dash (no LoS during dash — invulnerable to laser) |
| Cooldown | 2.5 seconds (resets on landing from any source) |
| Control during dash | None — direction locked at start |
| Weight gate | Lost at Moderate tier |

The i-frames are critical — dash is the "oh shit" escape move from an incoming laser. The Eye should see the Runner dash away. If dash had no invulnerability it would just be a repositioning tool, not a survival tool.

### 4.6 Air Dash

| Parameter | Value |
|-----------|-------|
| Input | `Shift + WASD direction` (airborne) |
| Duration | 0.15 seconds |
| Speed | 10 m/s horizontal, cancels vertical velocity during dash |
| Available once per airtime | Yes — `_airDashAvailable` resets on landing |
| Vertical effect | Sets `_vVelocity = 0` for duration (hover feel) |
| Weight gate | Lost at Light-Moderate threshold (> 5 kg) |

Air dash is the highest-skill ability — used to extend air time, dodge mid-air shots, and reach ledges that are slightly too far. Losing it at Light tier (> 5 kg) means even moderate looting eliminates the most expressive move. This is Pillar 2 in action.

### 4.7 Wall Run

| Parameter | Value |
|-----------|-------|
| Input | Sprint directly at angled wall (30–80° angle) |
| Entry condition | Speed > 5 m/s, wall height > 2.5 m, not Airborne falling at velocity > -3 m/s |
| Gravity reduction | 40% while wall running (player drifts down slowly) |
| Lateral speed | Preserves horizontal speed component along wall |
| Camera tilt | -15° toward wall (visual feedback, not gameplay) |
| Max duration | 1.5 seconds per wall segment |
| Cooldown between same-wall runs | 1 second |
| Weight gate | Lost at Heavy tier |

Wall run is for crossing gaps and bypassing vertical obstacles — not sustained vertical climbing (Wall Jump handles that). The 1.5s limit prevents degenerate "run forever on walls" gameplay that would break map design.

### 4.8 Grapple Hook

| Parameter | Value |
|-----------|-------|
| Input | `E` while aiming at grapple anchor in range |
| Item dependency | Requires `GrapplingHook` shrine item equipped |
| Anchor detection | Raycast from camera center, max 15m, `GrappleAnchor` layer |
| Swing physics | Simplified pendulum — angular velocity around anchor point |
| Release | Release `E` at apex for maximum speed launch |
| Weight | Grapple itself weighs 3 kg (adds to Runner's total) |
| Anchor reset | Each anchor resets after 5 second cooldown |

The grapple's Bezier arc swing should feel physics-based even though it's simplified. The "release at apex for speed" mechanic creates a skill expression that rewards reading swing timing. Anchors are placed by map designers at map generation time (SYS-10/SYS-12).

---

## 5. Input Mapping (New Input System)

All input goes through Unity's **Input System** (`InputAction` assets). Never use `Input.GetKey`.

```csharp
// RunnerInputReader.cs — no MonoBehaviour inheritance, just reads actions
public class RunnerInputReader
{
    public Vector2 MoveInput     { get; private set; }  // WASD
    public Vector2 LookInput     { get; private set; }  // Mouse delta
    public bool    SprintHeld    { get; private set; }
    public bool    JumpPressed   { get; private set; }  // consumed each frame
    public bool    JumpHeld      { get; private set; }
    public bool    SlidePressed  { get; private set; }
    public bool    GrappleHeld   { get; private set; }
    public bool    DashPressed   { get; private set; }
    public bool    InteractPressed { get; private set; } // pickup/drop

    // Cleared each tick — prevents "held" inputs from re-firing as "pressed"
    public void ConsumeJump()    => JumpPressed = false;
    public void ConsumeDash()    => DashPressed = false;
    public void ConsumeInteract() => InteractPressed = false;
}
```

The `RunnerInputReader` is only enabled when `IsOwner` (SYS-33 rule). It is disabled at `PostMatch` (SYS-22 contract).

---

## 6. FishNet Client-Side Prediction

This is the most technically complex part of the system. Full implementation detail is locked here, not in SYS-33 (which defined the contract). SYS-33 says prediction IS used for Runner movement; this GDD defines how.

### 6.1 Prediction Data Structs

```csharp
// Input packet sent to server each tick
public struct RunnerReplicateData : IReplicateData
{
    public Vector2 MoveInput;
    public Vector2 LookInput;
    public bool    JumpPressed;
    public bool    SlideHeld;
    public bool    SprintHeld;
    public bool    DashPressed;
    public bool    AirDashPressed;
    public bool    GrappleHeld;
    // Tick data (filled by FishNet)
    private uint _tick;
    public void Dispose() {}
    public uint GetTick() => _tick;
    public void SetTick(uint value) => _tick = value;
}

// Authoritative state sent from server for reconciliation
public struct RunnerReconcileData : IReconcileData
{
    public Vector3       Position;
    public float         VerticalVelocity;
    public Vector3       HorizontalVelocity;
    public MovementState State;
    public bool          IsGrounded;
    public bool          AirDashAvailable;
    public bool          DoubleJumpAvailable;
    // Tick data
    private uint _tick;
    public void Dispose() {}
    public uint GetTick() => _tick;
    public void SetTick(uint value) => _tick = value;
}
```

### 6.2 Prediction Methods on RunnerController

```csharp
public class RunnerController : NetworkBehaviour
{
    // Called by FishNet on owner (predict) and server (authoritative simulate)
    [Replicate]
    private void Move(RunnerReplicateData input,
                      ReplicateState state = ReplicateState.Invalid,
                      Channel channel = Channel.Unreliable)
    {
        // Deterministic movement simulation — same code runs on client AND server
        // No Time.deltaTime here — use TimeManager.TickDelta
        float delta = (float)TimeManager.TickDelta;

        ProcessLook(input.LookInput);
        ProcessMovement(input, delta);
    }

    // Called by FishNet on owner when server sends authoritative state
    [Reconcile]
    private void Reconcile(RunnerReconcileData data,
                            Channel channel = Channel.Unreliable)
    {
        // FishNet rewinds and replays from the reconcile point
        transform.position   = data.Position;
        _vVelocity           = data.VerticalVelocity;
        _hVelocity           = data.HorizontalVelocity;
        _movementState       = data.State;
        _airDashAvailable    = data.AirDashAvailable;
        _doubleJumpAvailable = data.DoubleJumpAvailable;
    }

    public override void TimeManager_OnTick()
    {
        // Fire every tick (not per-frame)
        if (IsOwner)
        {
            Reconcile(default);  // reconcile first
            RunnerReplicateData input = _inputReader.BuildReplicateData();
            Move(input);
        }
        if (IsServer)
        {
            Move(default);   // server replays inputs
            RunnerReconcileData state = BuildReconcileData();
            Reconcile(state);
        }
    }
}
```

### 6.3 Reconciliation Tolerance

FishNet will reconcile (snap position + replay) if server position diverges from client prediction. Acceptable divergence before snap: **0.1m**. At 30Hz tick rate with ~150ms relay RTT (4–5 ticks), minor divergence is expected and snaps are invisible if under tolerance.

If snaps are visually noticeable in playtesting, add position interpolation: `Vector3.Lerp(currentPos, reconciledPos, snapSpeed * delta)` instead of hard assignment. This is a polish tweak, not MVP.

### 6.4 Non-Owner Runner Rendering

Remote Runners (seen from the Eye or other Runners) use `NetworkTransform` interpolation (configured in SYS-33). Their `RunnerController.Move()` does NOT run on non-owner clients — only position is replicated, not the full state machine. Remote Runners will look slightly different in movement (no prediction, interpolated) — this is acceptable and not visible from the Eye's 2D view.

---

## 7. Camera Setup

### 7.1 First-Person Camera

- `Camera` attached to `CameraMount` child GameObject at head position (1.65m height)
- `CinemachineCamera` (Cinemachine 3) with a `CinemachineInputAxisController` for mouse look
- `LookInput` from `RunnerInputReader` fed into Cinemachine's axis controller

**Camera is ONLY enabled on the owning client** (SYS-33 rule):

```csharp
public override void OnStartClient()
{
    base.OnStartClient();
    if (!IsOwner)
    {
        _camera.enabled = false;
        _audioListener.enabled = false;
        return;
    }
    _cinemachineCamera.enabled = true;
}
```

### 7.2 Camera Tilt During Wall Run

Wall running tilts the camera -15° toward the wall for feel. This is client-side visual only — does not affect gameplay colliders or LoS raycasts:

```csharp
// In Update() on owner only — cosmetic
if (_movementState == MovementState.WallRunning)
{
    float tilt = _wallRunDirection == WallSide.Right ? -15f : 15f;
    _cameraMount.localRotation = Quaternion.Lerp(
        _cameraMount.localRotation,
        Quaternion.Euler(0, 0, tilt),
        Time.deltaTime * 8f
    );
}
else
{
    _cameraMount.localRotation = Quaternion.Lerp(
        _cameraMount.localRotation,
        Quaternion.identity,
        Time.deltaTime * 12f
    );
}
```

### 7.3 Head Bob (Optional — Polish)

Subtle camera bob during sprint. Implemented in a `HeadBobController` component, disabled by default, tunable in settings. NOT MVP.

---

## 8. Weight Tier Integration (SYS-04 Interface)

The Movement Controller does not own weight — it **reads** weight tier from the Weight/Burden System (SYS-04). The interface is:

```csharp
// RunnerController reads this every tick
private WeightTier _weightTier; // set via SyncVar callback from SYS-04

// Called when SYS-04's _weightTier SyncVar changes
private void OnWeightTierChanged(WeightTier prev, WeightTier next, bool asServer)
{
    _weightTier = next;
    // Immediately recalculate which abilities are available
    RefreshAbilityAvailability();
}

private void RefreshAbilityAvailability()
{
    _slideAvailable     = _weightTier < WeightTier.Heavy;
    _wallJumpAvailable  = _weightTier < WeightTier.Heavy;
    _wallRunAvailable   = _weightTier < WeightTier.Heavy;
    _dashAvailable      = _weightTier < WeightTier.Moderate;
    _doubleJumpAvailable= _weightTier < WeightTier.Moderate && _doubleJumpAvailable;
    _airDashAvailable   = (_weightTier.Weight() <= 5f) && _airDashAvailable;
    // Speed multiplier:
    _speedMultiplier = GlobalRules_SO.Instance.GetSpeedMultiplier(_weightTier);
}
```

**Design note:** When a Runner drops items mid-movement (SYS-09), their weight tier can change mid-air. The ability flags update immediately via the callback — if they just dropped enough weight to re-enable Air Dash while airborne, `_airDashAvailable` should check if it was ALREADY consumed this airtime. Dropping weight doesn't refill the one-per-airtime Air Dash.

---

## 9. State Transition Events (for VFX and Audio)

The Movement Controller fires events that SYS-32 (VFX) and AudioManager subscribe to. These are **local-only events** (fire only on the owning client) — each client shows VFX for their own Runner, plus interpolated NetworkTransform on others:

```csharp
// Fired on transitions — VFX and audio systems subscribe
public event Action OnLanded;           // ground contact after airtime
public event Action OnJumped;           // jump launch
public event Action OnSlideStarted;     // slide entry
public event Action OnSlideEnded;
public event Action OnWallRunStarted;
public event Action OnWallRunEnded;
public event Action OnDashStarted;
public event Action OnGrappleAttached;
public event Action OnGrappleReleased;
public event Action<float> OnLandedHard;  // fall distance > 3m — harder thud

// Also fired for OTHER clients' runners (non-owner) via ObserversRpc
// so all players can hear Runners landing/jumping in their vicinity
```

---

## 10. Movement Tuning Values

All values are stored in `GlobalRules_SO` under a `MovementConfig` nested struct. **Zero hardcoded values in movement code.**

```csharp
[System.Serializable]
public struct MovementConfig
{
    // ── Grounded ──────────────────────────
    public float walkSpeed;           // 4.0
    public float sprintSpeed;         // 7.5
    public float acceleration;        // 15.0
    public float deceleration;        // 20.0

    // ── Airborne ──────────────────────────
    public float gravity;             // 20.0 (custom, not Physics.gravity)
    public float jumpHeight;          // 2.5
    public float coyoteTime;          // 0.15
    public float jumpBuffer;          // 0.10

    // ── Slide ─────────────────────────────
    public float slideMinEntrySpeed;  // 4.0
    public float slideImpulse;        // 2.0
    public float slideFriction;       // 0.3
    public float slideExitMinSpeed;   // 2.0
    public float slideMaxDuration;    // 3.0
    public float slideJumpBoost;      // 1.3 (multiplier)

    // ── Dash ──────────────────────────────
    public float dashSpeed;           // 12.0
    public float dashDuration;        // 0.20
    public float dashCooldown;        // 2.5
    public float airDashSpeed;        // 10.0
    public float airDashDuration;     // 0.15

    // ── Wall Run ──────────────────────────
    public float wallRunMinEntrySpeed; // 5.0
    public float wallRunGravityScale;  // 0.4
    public float wallRunMaxDuration;   // 1.5
    public float wallRunCooldown;      // 1.0

    // ── Wall Jump ─────────────────────────
    public float wallJumpHorizontal;  // 4.5
    public float wallJumpVertical;    // 6.0
    public int   wallJumpChainLimit;  // 2

    // ── Weight Tier Speed Multipliers ─────
    public float speedMultiplierNaked;       // 1.00
    public float speedMultiplierLight;       // 0.90
    public float speedMultiplierModerate;    // 0.75
    public float speedMultiplierHeavy;       // 0.55
    public float speedMultiplierOverloaded;  // 0.35
}
```

---

## 11. Network Visibility: Eye Cannot Predict Runner Position

An important constraint for the Eye side: the Eye's client does NOT run `RunnerController.Move()` for Runners it's watching. The Eye sees Runner positions via `NetworkTransform` interpolation (30Hz updates). This means:

- From the Eye's perspective, Runners may appear to "jump" into cover 1–2 frames before the server says they're hidden
- This is expected and acceptable — LoS (SYS-05) is computed server-side anyway
- The Eye's view is intentionally slightly delayed — this is a design affordance, not a bug

---

## 12. Death State

When the server sets `IsAlive = false` (via `GameManager.NotifyRunnerKilled`):

```csharp
// SyncVar callback — fires on all clients
private void OnIsAliveChanged(bool prev, bool next, bool asServer)
{
    if (!next)
        EnterDeadState();
}

private void EnterDeadState()
{
    _movementState = MovementState.Dead;
    if (IsOwner)
    {
        _inputReader.Disable();        // no more input
        _cinemachineCamera.enabled = false;
    }
    // SYS-18 (Minion System) picks up from here — new NetworkObject spawned
}
```

The Runner `NetworkObject` is despawned by the server after the death animation plays (0.5s). `EnterDeadState()` freezes the character in place for that duration.

---

## 13. Prototype Test Plan

Before integrating FishNet or weight, build the movement controller against this checklist:

| Test | Pass Condition |
|------|---------------|
| Sprint acceleration | Feels snappy, reaches full speed in 0.5s |
| Jump height | Can clear a 2.5m obstacle from standing |
| Coyote time | Can jump 0.1s after walking off a ledge |
| Slide → jump | Noticeably faster than a standing jump (visible arc difference) |
| Double jump | Reaches 4m peak height (standing + double) |
| Wall jump | Can chain 4 wall jumps up an 8m shaft |
| Dash i-frames | Laser fires during dash → not hit |
| Air dash | Can extend 2m horizontal gap from apex of normal jump |
| Wall run | Traverses a 5m gap at 3m height using wall run |
| Weight tier speed | Overloaded feels painfully slow vs Naked |
| All combos | Slide → jump → wall run → wall jump → air dash in sequence, no state conflicts |

---

## 14. MVP Scope

- [x] CharacterController with custom velocity model decided
- [x] MovementState enum defined (8 states)
- [x] All 9 abilities specified (Sprint, Jump, Slide, WallJump, Dash, AirDash, WallRun, DoubleJump, Grapple)
- [x] FishNet prediction structs defined (ReplicateData, ReconcileData)
- [x] Prediction method signatures (`[Replicate]`, `[Reconcile]`, `TimeManager_OnTick`)
- [x] Input system pattern (RunnerInputReader, consumed fields)
- [x] Weight tier interface (callback from SYS-04, RefreshAbilityAvailability)
- [x] Camera setup (owner-only, Cinemachine 3, wall-run tilt)
- [x] State transition events for VFX/Audio
- [x] All tuning values in GlobalRules_SO MovementConfig struct
- [x] Death state handling
- [x] Prototype test plan (13 tests)
- [ ] Grapple swing physics implementation (basic pendulum — tuning in prototype)
- [ ] Head bob (Polish tier — not MVP)
- [ ] Landing camera shake (Polish tier)
- [ ] Footstep audio integration (SYS-19 dependency)

---

*System: SYS-01 — Runner Movement Controller*  
*GDD Version: 1.0*  
*Created: 2026-06-23*  
*Depends on: [sys-33-fishnet-replication.md](sys-33-fishnet-replication.md), [sys-22-match-state-machine.md](sys-22-match-state-machine.md)*  
*Status: Complete — prototype immediately*
