# Water Physics System

A probe-based buoyancy and wave simulation built on top of Stylized Water 3's Gerstner wave solver. The system is split into **force calculation** (WaterForces), **force application** (ApplyWaterForces variants), and **boat-specific dynamics** (BoatDynamics + BoatDriver).

---

## Architecture Overview

```
┌─────────────────────────────────────────────────────────────────────┐
│                        Per-FixedUpdate Flow                         │
│                                                                     │
│  BoatDriver (player/AI input)                                       │
│       │ calls BoatDynamics.SetInput(throttle, turning)              │
│       ▼                                                             │
│  BoatDynamics.FixedUpdate()                                         │
│       │ reads WaterForces.LastVelocity/SubmergedRatio               │
│       │   (from previous frame's GetForces call)                    │
│       │ applies thrust, rudder, planing, banking via                │
│       │   WaterForces.AddForce() / AddTorque()                      │
│       ▼                                                             │
│  ApplyWaterForcesBase coroutine (yields WaitForFixedUpdate)         │
│       │ calls WaterForces.GetForces(PhysicsState)                   │
│       │   → CalculateForces() [cached per frame]                    │
│       │   → runs ForceFilters                                       │
│       │ applies result to Rigidbody / Transform                     │
│       ▼                                                             │
│  Physics step complete                                              │
└─────────────────────────────────────────────────────────────────────┘
```

### Core Design Principles

1. **Separation of concerns** — `WaterForces` only calculates forces; an `ApplyWaterForces*` sibling applies them.
2. **Frame caching** — `GetForces()` computes once per fixed frame; multiple consumers read the same result.
3. **Late apply** — The applier runs as a coroutine yielding `WaitForFixedUpdate`, ensuring all `FixedUpdate` producers (BoatDynamics, external systems) have submitted their forces before integration.
4. **Probe-based** — Buoyancy, drag, and wave push are all derived from a configurable set of sample points (probes) attached to the object.

---

## Component Reference

### WaterForces

**File:** `Assets/05 Scripts/WaterPhysics/WaterForces.cs`

The central force calculator. Does **not** move the object — it only produces a `ForceResult` (linear force, torque, submerged ratio).

#### Key Concepts

| Concept | Description |
|---------|-------------|
| **Probes** | Local-space sample points that detect submersion. Each probe has a `localPos` and a normalized `weight`. |
| **Center of Mass** | A configurable `centerOfMass` offset used as the torque reference point. |
| **Hull Depth** | The depth at which a probe is considered fully submerged. Buoyancy ramps linearly from 0 (at waterline) to 100% (at `hullDepth`). |
| **Draft** | Vertical offset that raises or lowers the resting point relative to the water surface. |
| **PhysicsState** | A struct of `{position, rotation, velocity, angularVelocity}` passed in by the applier each frame. |

#### Force Calculation Pipeline (CalculateForces)

The method runs once per fixed frame and produces a `ForceResult`:

```
1. Gravity          →  F = mass × 9.81 × gravityMultiplier
2. Per-Probe Loop:
   a. Sample water height at probe world position
   b. Compute displacement = probe.y − waterHeight − draft
   c. If submerged (displacement < 0):
      - Buoyancy force (proportional to depth)
      - Vertical drag (opposing probe's vertical velocity)
      - Exit drag (extra drag when surfacing, scales with surface proximity)
      - Slamming force (stopping-force on rapid entry)
      - Torque from buoyancy around CoM
      - Wave push (PerProbe mode only)
3. CenterOfMass wave push (if using CenterOfMass mode)
4. Wave push post-processing pipeline:
   a. Smoothing (rolling average over N frames)
   b. Clamping (freeze direction/magnitude when force drops below threshold)
   c. Strength multiplier + angle-based multiplier
   d. Damping (exponential decay toward target)
5. Horizontal drag (anisotropic: forward vs lateral)
6. Angular drag (anisotropic: yaw vs pitch/roll)
7. Drain pending external forces (AddForce/AddTorque API)
8. Run ForceFilters
```

#### Buoyancy

Each submerged probe contributes an upward force proportional to how deep it is below the waterline:

```
submersionRatio = clamp(displacement / hullDepth, 0, 1)
probeForce = submersionRatio × buoyancyForce × weight
```

The force is applied at the probe's world position, which naturally creates a torque that rights the object. A probe deeper on one side pushes harder, rotating the hull level.

#### Vertical Drag

Opposes the probe's vertical velocity (including rotational contribution):

```
pointVel = velocity + angularVelocity × offsetFromCoM
probeForce -= pointVel.y × verticalDrag × weight
```

When `exitDrag > 0` and the probe is moving **upward**, additional drag is added that scales with surface proximity (1 at waterline → 0 when fully submerged). This simulates the "suction" effect of water resisting a hull pulling out.

#### Slamming

A stopping-force approach inspired by Avalanche's JC3 water physics:

```
if (downwardSpeed > threshold):
    intensity = pow(downwardSpeed / rampSpeed, slammingPower)
    stoppingForce = mass × downwardSpeed / dt
    probeForce += stoppingForce × intensity × weight
```

This is inherently stable — the force can never exceed what would halt the probe's downward motion in one frame, so it cannot overshoot.

> **Note:** Slamming is currently disabled on the player boat (`slammingRunSpeed` / `slammingThreshold` set to zero). The visual quality remains acceptable because the gravity multiplier was increased to 3×, which creates a similar effect by driving the hull deeper in a single frame and producing stronger probe responses.

#### Wave Push

Drives the object horizontally based on wave slope and vertical water velocity. Two quality modes:

| Mode | Extra Samples | Description |
|------|--------------|-------------|
| **Off** | 0 | No wave push. |
| **CenterOfMass** | 2 total | Samples wave slope once at the center of mass. No torque. |
| **PerProbe** | 2 per submerged probe | Samples wave slope at every submerged probe. Generates torque. |

**Slope computation:** For each sample point, the water surface normal is computed by sampling the height at the point and at two offset positions (+X and +Z by `normalSampleOffset`). The horizontal component of the normal gives the slope direction.

**Velocity modes:**
- **Absolute** — Push scales with water's vertical velocity alone. Consistent strength.
- **Relative** — Push scales with the difference between water velocity and the object's velocity. Weaker when buoyancy tracks the surface well.

**Backside behaviour** controls what happens when the water height is dropping (backside of a wave):
- `PositiveForce` — Default: slope direction naturally produces a backward push.
- `NegativeForce` — Multiplied by `waveBacksideMultiplier` (typically negative, creating a pull-back).
- `Diminish` — Force zeroed.

**Wave push post-processing pipeline:**

1. **Smoothing** — Rolling average over `smoothingFrames` frames. Reduces jitter.
2. **Clamping** — When the smoothed force drops below `wavePushMinMagnitude`, holds the last valid magnitude and/or direction for up to `wavePushClampFalloffFrames` frames. Prevents wave push from flickering on/off near zero.
3. **Strength & angle multiplier** — Multiplied by `wavePushStrength`, then optionally by an angle-based curve from `WavePushAngleConfig` (see below).
4. **Damping** — Exponential decay (`Lerp` with `1 - e^(-speed × dt)`) toward the target force. Smooths transitions without adding latency.

Note: This pipeline operates internally on wave push forces during `CalculateForces()`. After all forces (including wave push) are summed, the separate **ForceFilter** system runs on the combined `ForceResult` — see the Force Filters section below.

#### Horizontal Drag (Anisotropic)

Applied at the center of mass when any probe is submerged. Decomposes horizontal velocity into forward and lateral components relative to the object's heading:

```
fwdVel = Dot(horizontalVel, heading) × heading
latVel = horizontalVel − fwdVel
```

Each component is damped independently by `forwardDrag` and `lateralDrag`. Supports linear (`F = -drag × v`) and quadratic (`F = -drag × |v| × v`) modes.

If `scaleDragWithSubmersion` is true, drag scales with the submerged ratio (partial submersion = partial drag). Otherwise it's binary (any probe submerged = full drag).

#### Angular Drag (Anisotropic)

Resists rotation when submerged, split into yaw (Y axis) and tilt (X + Z axes):

```
localAngVel = Inverse(rot) × angularVelocity
localDragTorque = (-localAngVel.x × tiltDrag, -localAngVel.y × yawDrag, -localAngVel.z × tiltDrag)
```

Converted back to world space and added to the torque accumulator.

#### External Force API

Other systems can contribute forces each frame via:

- `AddForce(Vector3)` — Adds linear force.
- `AddTorque(Vector3)` — Adds torque.
- `AddForceAtPosition(Vector3, Vector3)` — Adds force at a world position, computing the resulting torque around CoM.

These are drained (reset to zero) each frame after being incorporated into the result.

#### Force Filters

Post-processors that run after all forces are calculated. Stored in a `[SerializeReference]` array, enabling polymorphic serialization. Built-in filters:

| Filter | Purpose |
|--------|---------|
| **CapsizingPreventionFilter** | Spring-damper that counteracts pitch/roll beyond a threshold angle. Uses `SmoothStep` to ramp engagement between min and max trigger angles. |
| **ClampForces** | Hard caps on linear force and torque magnitudes. |
| **ScaleForces** | Simple multiplier on force and torque (e.g., for difficulty scaling). |
| **VerticalRangeFilter** | Safety net preventing the object from flying too high or sinking too deep. Applies a spring force plus velocity damping when outside the allowed range. |

Custom filters subclass `ForceFilter`, mark `[System.Serializable]`, and implement `FilterForces(ref ForceResult, WaterForces)`.

---

### ApplyWaterForcesBase

**File:** `Assets/05 Scripts/WaterPhysics/ApplyWaterForcesBase.cs`

Abstract base class for force appliers. Manages a coroutine that yields `WaitForFixedUpdate` then calls `OnApplyForcesStep(dt)`. This ensures all `FixedUpdate` producers have finished before forces are integrated.

**Shared features:**
- `applyPosition` / `applyRotation` — `AxisMask` flags to selectively enable/disable individual axes.
- `dampingEpsilon` — Tiny velocity decay to absorb floating-point drift (separate from water drag).
- `Teleport(pos, rot, resetMomentum)` — Repositions the object instantly.
- `AddImpulse(Vector3)` — Applies an instantaneous velocity change.

#### Variants

| Applier | Target | Integration | Notes |
|---------|--------|-------------|-------|
| **ApplyWaterForcesToDynamicRigidbody** | Non-kinematic Rigidbody | `AddForce` / `AddTorque` | Disables Rigidbody gravity (WaterForces handles it). Sets CoM and inertia. Uses `BoatMaterial` physic material (`Assets/04 Data/Physics/BoatMaterial.physicMaterial` — low friction: 0.1 dynamic/static, bounciness 0.5, friction combine = Min, bounce combine = Max). |
| **ApplyWaterForcesToKinematicRigidbody** | Kinematic Rigidbody | Manual via `MovePosition` / `MoveRotation` | Includes overlap-depenetration collision. Can push dynamic bodies. |
| **ApplyWaterForcesToTransform** | Transform (no Rigidbody) | Manual | Includes smooth visual interpolation in `Update`/`LateUpdate`. |

#### ApplyWaterForcesManualBase

**File:** `Assets/05 Scripts/WaterPhysics/ApplyWaterForcesManualBase.cs`

Base for kinematic Rigidbody and Transform appliers. Performs explicit Euler integration:

1. **Linear:** `velocity += (force / mass) × dt`, then `position += velocity × dt`
2. **Angular:** Converts torque to local space, divides by per-axis inertia (`inertia × mass`), integrates angular velocity, and applies via axis-angle rotation to avoid gimbal lock.

Supports `UnityStyle` (`1 - damping × dt`) and `Exponential` (`e^(-damping × dt)`) damping modes.

#### ApplyWaterForcesToKinematicRigidbody — Collision

Uses an **overlap-depenetration** approach rather than sweep casting:

1. After integrating forces, tests the final pose (position + rotation) against scene geometry using `Physics.OverlapBoxNonAlloc`.
2. For each overlap, `Physics.ComputePenetration` computes the exact minimum separation vector.
3. The object is pushed out along that vector (plus `skinWidth`).
4. Velocity is deflected: the component going into the surface is removed and optionally bounced by `bounciness`.
5. Runs up to `maxCollisionIterations` passes to handle multiple simultaneous contacts.
6. Dynamic bodies encountered during resolution receive a proportional push impulse.

This catches rotation-induced collisions (e.g., bow swinging into a rock) that pure sweep-based approaches miss.

Additionally, when `receiveCollisions` is enabled, `OnCollisionEnter`/`Stay` callbacks from dynamic bodies hitting the kinematic boat are forwarded as forces into WaterForces.

---

### WaterSampler

**File:** `Assets/05 Scripts/WaterPhysics/System/WaterSampler.cs`

Singleton that wraps Stylized Water 3's Gerstner wave computation. Key features:

- **Frame caching** — `ComputeHeight(x, z)` results are cached per fixed frame. Repeated calls for the same coordinates return instantly.
- **Frame data** — Wave parameters (direction, speed, frequency, layers, scale) are read from the material once per fixed frame.
- **Global offset** — Applies `_WaterPositionOffset` so animated wave movement is reflected in CPU-side height queries.
- **GetSeaRoughnessNormalised()** — Returns a 0–1 roughness value based on the wave profile's amplitude multiplier.

---

### BoatDynamics

**File:** `Assets/05 Scripts/CharacterControl/Boat/Shared/BoatDynamics.cs`

Concrete boat dynamics engine: thrust, steering, planing, banking, and anchor. Receives input from `BoatDriver` subclasses via `SetInput(throttle, turning)`.

> **Note:** The player boat runs with `gravityMultiplier` set to **3×** (mass × 9.81 × 3) to prevent excessive airtime over waves.

#### Input Processing

1. **Throttle smoothing** — `MoveTowards` with separate ramp-up and ramp-down rates.
2. **Turning inversion** — When reversing (`throttle < 0`), turning input is inverted so the rudder feels natural.
3. **Boost** — `SetBoostInput(thrust, torqueReduction)` adds extra thrust and optionally reduces rudder torque (for boost mechanics).

#### Force Contributions (per FixedUpdate)

| Force | Description |
|-------|-------------|
| **Thrust** | `direction × (curvedThrottle × thrustForce + accelBoost + boostThrust)`. Gated by submersion. Reversing scales by `reverseThrust`. |
| **Rudder torque** | `turning × rudderTorque × rudderCurve(normalizedSpeed) × throttleBoost × mass`. The rudder speed curve reduces effectiveness at low speed. |
| **Acceleration boost** | Extra thrust at low speed (evaluated from `accelerationBoostCurve` based on `currentSpeed / maxSpeed`). |
| **Planing** | Pitch torque at speed — the bow rises as the boat planes. Evaluated from `planingCurve`. |
| **Banking** | Cosmetic roll into turns. Smoothed with exponential decay. |
| **Anchor spring** | When anchored, a spring-damper pulls the boat toward the anchor point. Slack radius allows free movement within range. |

#### Stats (BoatStats ScriptableObject)

All tuning values come from a `BoatStats` SO. `ApplyStats()` copies values into runtime fields and **overwrites** WaterForces settings (drag, wave push, etc.) to keep everything in sync. This means the BoatStats SO is the single source of truth for a boat's physics parameters.

#### Max Speed Calculation

```
Linear drag:    maxSpeed = thrust / (forwardDrag × forceScale)
Quadratic drag: maxSpeed = sqrt(thrust / (forwardDrag × forceScale))
```

Where `thrust` = `throttleCurve(1.0) × thrustForce` and `forceScale` = `mass` if `scaleForcesWithMass`.

#### Direction Tracking

`BoatDynamics` maintains a `m_direction` vector (horizontal-only) that lerps toward `transform.forward` over time. This provides a smooth heading reference for thrust application, independent of wave-induced pitching.

---

### BoatDriver

**File:** `Assets/05 Scripts/CharacterControl/Boat/Shared/BoatDriver.cs`

Abstract base class for boat controllers. Subclasses compute throttle and turning values and call `Dynamics.SetInput(throttle, turning)`. Two concrete implementations:

---

### PlayerBoatDriver

**File:** `Assets/05 Scripts/CharacterControl/Boat/Player Boat/PlayerBoatDriver.cs`

Reads Unity InputSystem actions (keyboard or controller thumbstick). For controller input, throttle and turning are derived from the thumbstick angle using configurable dead-zone angles (`fullThrottleInputAngle`, `fullTurningInputAngle`) so the player doesn't need to be perfectly precise on the stick. Also handles game-system integration (POI registration, dialogue anchoring — auto-drops anchor during dialogue).

---

### NPCBoatDriver

**File:** `Assets/05 Scripts/CharacterControl/Boat/NPC Boat/NPCBoatDriver.cs`

AI-driven boat driver with a four-state state machine and PD-controlled steering/throttle.

#### State Machine

| State | Description |
|-------|-------------|
| **Idle** | No input fed to dynamics. Still scans for chase targets. |
| **FollowPath** | Follows waypoints from a `Route` component. Supports ping-pong direction. |
| **Chase** | Pursues a moving `chaseTarget` (typically the player). Uses NavMesh pathing when available. |
| **Anchor** | Drops anchor via `BoatDynamics.DropAnchor()`. Zero input fed. |

State transitions:
- **FollowPath → Chase**: When `chaseTarget` enters `detectionRadius`.
- **Chase → FollowPath**: When target leaves `escapeRadius`, or boat exceeds `maxChaseRadius` from the route centroid. Both trigger a cooldown timer to prevent instant re-detection.
- **Any → Anchor**: Via `SetState(NPCBoatState.Anchor)`.
- **Idle → Chase**: Same detection check as FollowPath.

#### Navigation

NPC boats follow a `Route` (a set of waypoints). When the Route has a baked NavMesh:

- **Dynamic avoidance** (`m_dynamicAvoidance`): Periodically computes a live NavMesh path from the boat to the current route waypoint. The boat steers along NavMesh corners instead of straight-lining to the waypoint, dodging `NavMeshObstacle` objects.
- **Chase pathing**: Computes a live NavMesh path to the chase target at `chaseNavMeshRefreshInterval`. When within `ramDistance`, live NavMesh is disabled and the boat beelines for the target.

When no NavMesh is available, the boat steers directly toward waypoints.

**Anticipation** blends the steering target toward the *next* waypoint as the boat approaches the current one, creating smooth arcs instead of hard turns. (Currently disabled due to NavMesh interaction issues.)

#### AI Input Computation (PD Controller)

Runs in `Update()`, produces throttle and turning values fed into `BoatDynamics.SetInput()`:

**Steering (PD control):**
```
proportional = steeringCurve(angle / fullAngle) × sign(angle)
derivative = yawRateDeg / yawDampingFullRate × yawDampingGain
turning = clamp(proportional − derivative, −1, 1)
```

The derivative term reads `WaterForces.LastAngularVelocity.y` (current yaw rate) and opposes the rudder to prevent overshoot. Higher `yawDampingGain` = smoother turns with less oscillation.

**Throttle (distance × alignment):**
```
distanceFactor = distanceCurve(distance / effectiveArrival)
alignmentFactor = alignmentCurve(dot(heading, targetDir) remapped to 0–1)
throttle = max(distanceFactor × alignmentFactor, minTurningThrottle × offHeadingFactor)
```

- The arrival distance is scaled by current forward speed so fast approaches brake earlier.
- `minTurningThrottle` prevents the boat from stopping dead during large heading corrections.
- A forward raycast checks for obstacles and eases throttle toward zero using a quadratic curve.

#### Fallback Movement (LOD Cheap Mode)

When `BoatDynamics.PhysicsActive` is false (BoatLOD has downgraded the boat), the NPCBoatDriver drives the Transform directly:

- `MoveTowards` the target at `fallbackMoveSpeed`.
- Yaw rotation via swing-twist decomposition, preserving wave tilt from `SimpleWaterAlign`.

This ensures NPC boats keep moving visually even without full physics simulation.

---

### WavePushAngleConfig

**File:** `Assets/05 Scripts/WaterPhysics/WavePushAngleConfig.cs`

A serializable struct that defines angle-based wave push multipliers using dot-product bands. Each `AngleBand` specifies a `[dotMin, dotMax]` range and a multiplier:

- Dot = +1: wave pushing from behind (same direction as boat)
- Dot = 0: wave pushing from the side
- Dot = -1: wave pushing head-on

When `enabled`, the dot product between the boat's forward heading and the wave push direction is evaluated against all bands. The matching band's multiplier scales the wave push force and torque. If no band matches, `fallbackMultiplier` is used.

---

### WaterProbePresets

**File:** `Assets/05 Scripts/WaterPhysics/WaterProbePresets.cs`

Static utility that generates common probe layouts:

| Preset | Points | Layout |
|--------|--------|--------|
| **Cross** | 4 | Front, back, left, right |
| **Square** | 4 | Four corners |
| **Triangle** | 3 | Forward point + two rear points (equilateral) |
| **Square8** | 8 | Four corners + four edge midpoints |

All weights start at 1.0 and are normalized by `WaterForces.NormalizeProbeWeights()` so they sum to 1.0.

---

### BoatProp

**File:** `Assets/05 Scripts/CharacterControl/Boat/Shared/BoatProp.cs`

Generic visual driver for boat props (propellers, rudders, flags, etc.). Reads state from a parent `BoatDynamics` and applies rotation. Attach to each visual element that should react to boat state.

**Input sources:** Throttle, TurningInput, Speed (normalized), HorizontalSpeed (normalized).

**Modes:**

| Mode | Description |
|------|-------------|
| **ContinuousSpin** | Rotates continuously at a speed proportional to the input value. Good for fans/propellers. Supports separate ramp-up/ramp-down rates and optional "turn spin when idle" (spins slightly during turning with no throttle). |
| **TargetAngle** | Lerps to a target angle proportional to the input value. Good for rudders. |

Runs in `LateUpdate` so it reacts after all physics and input are resolved.

---

### Boat Wake / Displacement System

The player boat uses Stylized Water 3's **displacement add-on** to render a boat wake. Andreas implemented a system that renders boat-shaped particles on each boat. The particles are affected by the Rigidbody's velocity but are not rendered in the Game — they live on the `BoatWake` layer. Each boat previously had its own top-down camera to render these particles to a render texture, which was then used for water displacement.

To optimise this, only the **player boat** now has this camera. The orthographic size was set large (~200) so it captures the BoatWake particles from both the player and nearby NPC boats. This works because distant NPC boats are too far away for their wake to be visible anyway.

> The `BoatWake` layer is **very expensive** — disabling it in the scene's layer visibility is recommended for editor performance.

---

### BoatLOD

**File:** `Assets/05 Scripts/WaterPhysics/BoatLOD.cs`

Three-tier LOD system for NPC boats:

| Tier | Method | Cost |
|------|--------|------|
| **FullPhysics** | WaterForces + BoatDynamics + applier | Full force calculation, collision |
| **TriangleSample** | SimpleWaterAlign (3-point) + NPC movement | CPU wave samples, no physics |
| **SinglePoint** | SimpleWaterAlign (1-point) + procedural bob | Minimal |

Distance thresholds use hysteresis to prevent flickering at boundaries. When transitioning back to FullPhysics, the applier is teleported to the current visual pose and given an impulse matching the velocity at the time of downgrade.

> **Note:** The current BoatLOD implementation is slated for a refactor. The existing approach (AddImpulse + teleport) is fragile; the planned simplification is to disable only the applier and NPC driver during cheap tiers, and move the boat on the XZ plane following its route while retaining the WaterForces calculation for visual alignment.

---

### SimpleWaterAlign

**File:** `Assets/05 Scripts/WaterPhysics/SimpleWaterAlign.cs`

Lightweight water alignment for LOD cheap tiers:

- **TriangleSample** — Three height samples (forward, back-left, back-right) derive a surface normal via cross product. Rotation is applied using swing-twist decomposition to preserve yaw.
- **SinglePoint** — One height sample at center, plus procedural sinusoidal bobbing with randomized phase offsets.

Both modes run in `LateUpdate` and apply smooth interpolation on height and rotation.

---

### WaveProfile (Stylized Water 3)

**File:** `Assets/08 Plugins/Stylized Water 3/Runtime/WaveProfile.cs`

A `ScriptableObject` that defines a set of Gerstner wave layers. Each `Wave` layer has:
- **waveLength** — distance between crests
- **amplitude** — height from base to peak
- **steepness** — horizontal displacement (too high causes crest looping)
- **direction** — travel angle in degrees
- **mode** — Directional (angle-based) or Radial (point-origin)
- **enabled** — whether the layer contributes

Global multipliers (`waveLengthMultiplier`, `amplitudeMultiplier`, `steepnessMultiplier`) scale all layers uniformly. Per-layer curves (`waveLengthCurve`, `amplitudeCurve`, `steepnessCurve`) apply additional scaling based on layer index. `steepnessClamping` normalizes steepness across layers to prevent crest looping.

When `UpdateShaderParameters()` is called, the profile writes all layer data into a lookup texture (LUT) — an 8×2 `RGBAHalf` texture read by the water shader on the GPU. This is the bridge between CPU-side wave data and GPU rendering. `WaterSampler` also reads the same profile data for CPU-side height queries.

#### ReferenceWaveProfile

**File:** `Assets/04 Data/WaterPhysics/ReferenceWaveProfile.asset`

The project's base wave template. WaveManager holds two profiles:
- **ReferenceProfile** — this asset, the "source of truth" for wave shapes.
- **ActiveProfile** — the runtime profile that WaterSampler and the water material read. Modified in place by WaveManager and WaveTweener.

The ReferenceProfile is **never modified at runtime**. On startup, `WaveManager.Awake()` clones its global properties onto the ActiveProfile. All procedural wave generation (`SetToRandomWaveProfile`) works by copying the ReferenceProfile's layers, rotating their directions, and jittering their amplitudes.

The ReferenceProfile has 8 layers with a specific naming convention:

| Index | Name | Purpose |
|-------|------|---------|
| 0 | **Major Direction** | Primary swell — longest wavelength (60m), highest amplitude (2.0), base direction (0°). Defines the dominant wave direction. |
| 1 | **Interference Wave** | Secondary swell — shorter wavelength (30m), moderate amplitude (1.0), slight angle offset (20°). Creates cross-swell interference patterns. |
| 2 | **Ripples Wave #1** | Surface detail — amplitude 0.25, perpendicular direction (90°). Adds visual complexity. |
| 3 | **Ripples Wave #2** | Surface detail — amplitude 0.3, diagonal direction (150°). Fills in texture from another angle. |
| 4–7 | **Lerp #1–4** | Reserve slots — amplitude at `MIN_AMPLITUDE` (0.012), effectively silent. Used by WaveTweener as the "incoming" set during crossfade transitions. |

The 4+4 split is critical: layers 0–3 are the "current" waves the player sees and the physics system samples. Layers 4–7 are the "incoming" waves that WaveTweener fades in during a transition. Once the crossfade completes, the incoming data is copied into layers 0–3 and reserves are reset to `MIN_AMPLITUDE`. This is why `ApplyLayersInstantly` always writes to layers 0–3 and clears 4–7.

---

### WaveManager

**File:** `Assets/05 Scripts/WaterPhysics/WaveManager.cs`

Singleton that controls two independent aspects of the wave system:

#### Global Amplitude

Controls how tall waves are overall. This is a single float (`amplitudeMultiplier`) on the active `WaveProfile` that scales all wave layers uniformly. It is **not** the wave shape — just the overall height multiplier.

Amplitude is calculated each frame using **Inverse Distance Weighting (IDW)** between the player boat's position and registered `WaveAmplitudeInfluenceArea` volumes (safe harbors, calm zones, etc.):

```
For each influence area:
    weight = 1 / (normalizedDistance² + ε)

Add a virtual "ocean" contributor with weight = OceanStrength and amplitude = OceanMaxAmplitude.

targetAmplitude = Σ(weight × area.amplitude) / Σ(weight)
```

This creates smooth transitions: near an influence area, its calm amplitude dominates; far from all areas, the open ocean amplitude takes over. The `OceanInfluenceRadius` normalizes the distance calculation, and `OceanStrength` controls how quickly the ocean wins.

The current amplitude exponentially decays toward the target at `AmplitudeBlendSpeed`:
```
current = target + (current - target) × e^(-blendSpeed × dt)
```

**Amplitude locking:** `LockGlobalAmplitude(value)` freezes the amplitude at a specific value (for cutscenes, scripted moments). `UnlockGlobalAmplitude()` resumes IDW-based calculation. Lock state overrides everything.

#### Wave Profile Transitions

Controls the *shape* of waves (direction, wavelength, steepness per layer) via the active `WaveProfile`'s layer array. Transitions are handled by `WaveTweener` (see below).

Key API:

| Method | Description |
|--------|-------------|
| `SetToRandomWaveProfile()` | Procedurally generate a profile by rotating the reference profile by a random angle and jittering amplitude by ±`AmplitudeChangePercentage`. **Primary method for gameplay use.** |
| `SetToRandomWaveProfileWithDirection(dir)` | Same but with a specific wave direction. **Primary method for gameplay use.** |
| `SetWaveProfileToReference()` | Snap back to the reference profile's layers. |
| `SetWaveProfileManually(profile)` | Apply a hand-authored profile's layers (max 4). Does not change amplitude. **Debug / cutscene use only** — not currently used at runtime. For when a very specific sea state is needed for a scripted moment. |

All methods support `instantSet` (snap immediately) or `customDuration` (smooth transition via WaveTweener).

#### WaveAmplitudeInfluenceArea

**File:** `Assets/05 Scripts/WaterPhysics/WaveAmplitudeInfluenceArea.cs`

A trigger collider that registers itself with WaveManager. Defines a volume where waves should be calmer. Distance to the player is computed via `Collider.ClosestPoint`, handling any collider shape automatically. Each area has its own `TargetAmplitude`.

---

### WaveTweener

**File:** `Assets/05 Scripts/WaterPhysics/WaveTweener.cs`

Singleton that smoothly crossfades between wave layer sets using Stylized Water 3's 8-layer system. The active `WaveProfile` has 8 layer slots — WaveTweener uses layers 0–3 as the "current" set and layers 4–7 as the "incoming" set.

#### Crossfade Mechanism

When `CrossfadeToLayers(targetLayers, duration)` is called:

1. Copy the incoming wave data into layers 4–7 (reserve slots), with amplitude set to `MIN_AMPLITUDE`.
2. Over `duration` seconds, simultaneously:
   - **Fade out** layers 0–3: amplitude lerps toward `MIN_AMPLITUDE` using `FadeOutCurve`.
   - **Fade in** layers 4–7: amplitude lerps toward target amplitude using `FadeInCurve`.
3. On completion, copy reserve layer data into base layers 0–3 and clear reserves.

The fade-in and fade-out phases have configurable start/end points within the transition timeline (`FadeOutStart/End`, `FadeInStart/End`), so the incoming waves can start appearing before the old ones have fully faded, creating an overlap period.

#### Interruption

If a new crossfade is requested while one is already running, the tweener:
1. Stops the current coroutine.
2. Runs a 1-second interrupt phase that fades out the reserve layers (4–7) to clean up any partial crossfade.
3. Starts a fresh crossfade to the new target.

A second interruption during the interrupt phase queues the target but does not restart — the interrupt must complete first.

#### WaveProfileUtility

**File:** `Assets/05 Scripts/WaterPhysics/WaveProfileUtility.cs`

Static helpers for copying `WaveProfile.Wave` layer data without allocations (`CopyWaveData`, `ResetToEmptyLayer`, `CloneWaveProfile`). Used by WaveManager and WaveTweener to manipulate the active profile in place.

---

### RotationUtils

**File:** `Assets/05 Scripts/WaterPhysics/RotationUtils.cs`

Provides `DecomposeSwingTwist` — splits a quaternion into swing (tilts the axis) and twist (rotates around the axis) components. Used throughout the system to preserve yaw while applying wave-induced pitch/roll.

---

## Data Flow: Complete Frame Sequence

### FixedUpdate Phase

```
FixedUpdate #N begins
│
├─ BoatDriver.Update() [can run in Update, sets input]
│   └─ Dynamics.SetInput(throttle, turning)
│
├─ BoatDynamics.FixedUpdate()
│   ├─ Reads WaterForces.LastVelocity (from frame N-1)
│   ├─ Reads WaterForces.SubmergedRatio (from frame N-1)
│   ├─ Throttle smoothing
│   ├─ Thrust → WaterForces.AddForce()
│   ├─ Rudder → WaterForces.AddTorque()
│   ├─ Planing → WaterForces.AddTorque()
│   ├─ Banking → WaterForces.AddTorque()
│   └─ Anchor spring → WaterForces.AddForce()
│
├─ [Other systems may also call AddForce/AddTorque here]
│
├─ ApplyWaterForces coroutine fires (yields WaitForFixedUpdate)
│   ├─ WaterForces.GetForces(state)
│   │   ├─ WaterSampler.ComputeHeight() for each probe
│   │   │   (reads wave shape from ActiveProfile, which WaveTweener
│   │   │    may be crossfading — see Update phase below)
│   │   ├─ Buoyancy, drag, slamming per submerged probe
│   │   ├─ Wave push computed and post-processed
│   │   ├─ Horizontal + angular drag
│   │   ├─ Pending external forces drained
│   │   ├─ ForceFilters run on combined result
│   │   └─ Result cached, LastVelocity/SubmergedRatio updated
│   └─ Applies force to Rigidbody / integrates manually
│
└─ FixedUpdate #N complete
```

### Update Phase

```
Update
│
├─ WaveManager.Update()
│   ├─ Enforce GlobalSteepness on ActiveProfile
│   └─ UpdateAmplitude()
│       ├─ If amplitude locked → target = locked value
│       ├─ Else → CalculateIDWAmplitude()
│       │   ├─ For each WaveAmplitudeInfluenceArea:
│       │   │   weight = 1 / (dist²/radius² + ε)
│       │   ├─ Virtual ocean contributor (OceanStrength × OceanMaxAmplitude)
│       │   └─ target = weightedSum / totalWeight
│       └─ current = exponential decay toward target
│           → ActiveProfile.amplitudeMultiplier = current
│           → ActiveProfile.UpdateShaderParameters()
│
├─ WaveTweener coroutine (if crossfading)
│   └─ Lerp amplitude: layers 0–3 fade out, layers 4–7 fade in
│       → ActiveProfile.UpdateShaderParameters()
│       (WaterSampler reads this same profile next FixedUpdate)
│
├─ NPCBoatDriver.Update() [if NPC boat]
│   ├─ State machine transitions
│   ├─ NavMesh path refresh
│   ├─ ComputeAIInputs (PD steering/throttle)
│   ├─ Dynamics.SetInput(throttle, turning)
│   └─ FallbackMove if PhysicsActive == false
│
├─ PlayerBoatDriver.Update() [if player boat]
│   ├─ Read InputSystem actions
│   └─ Dynamics.SetInput(throttle, turning)
│
├─ ApplyWaterForcesToTransform.PerformInterpolation()
│   └─ Smooth visual position/rotation between fixed steps
│
└─ SimpleWaterAlign.LateUpdate() [LOD cheap tiers only]
```

---

## Probe System In Detail

### How Probes Work

Probes are local-space points sampled against the water surface each frame. A probe is **submerged** when its world Y position is below the water height (minus draft).

```
worldPos = objectPosition + objectRotation × (probe.localPos × objectScale)
waterHeight = WaterSampler.ComputeHeight(worldPos.x, worldPos.z)
displacement = worldPos.y - waterHeight - draft
submerged = displacement < 0
```

The displacement is clamped to `[-hullDepth, 0]` so buoyancy maxes out at full submersion.

### Weight Normalization

All probe weights are normalized to sum to 1.0 (`NormalizeProbeWeights()`). This means:
- Adding more probes doesn't increase total force.
- A probe with weight 0.5 contributes half as much buoyancy/drag as one with weight 1.0 (before normalization).
- The submerged ratio is calculated as `sum(submerged probe weights) / totalWeight`.

### Choosing a Probe Layout

- **Cross (4)** — Good default. Captures pitch and roll independently.
- **Square (4)** — Better for rectangular hulls.
- **Triangle (3)** — Minimal for small/cheap objects.
- **Square8 (8)** — Highest fidelity for large hulls where the water surface varies significantly across the hull.

More probes = more `ComputeHeight` calls per frame. The CenterOfMass wave push mode adds only 2 extra samples total; PerProbe adds 2 per submerged probe.

---

## Debug Tools

### DraggableDebugWindow

**File:** `Assets/05 Scripts/Utility/DraggableDebugWindow.cs`

A reusable runtime IMGUI window. Draggable, toggle-able via hotkey, with position and visibility persisted to `EditorPrefs` between play sessions (editor only). Provides styled helpers for common layouts — `Header()`, `Row()`, `BannerHeader()`, `DrawBar()` — with a dark theme that matches Unity's default look.

Used by the water physics test scenes and other debug overlays throughout the project.

### TestSceneDebugWindow

**File:** `Assets/05 Scripts/Utility/TestSceneDebugWindow.cs`

A `MonoBehaviour` that wraps `DraggableDebugWindow` for zero-code test scene setup. Drop it on any GameObject, configure the Inspector fields:

- **Scene description** — A text area explaining what the test scene demonstrates.
- **Quick-action buttons** — An array of named `UnityEvent` entries that appear as clickable buttons.

Toggle with F1 (configurable). Used in the water physics test scenes to let designers quickly toggle wave push modes, reset positions, switch profiles, etc. without writing code.