# 10 — Stress / Sanity System (`BPC_StressSystem`)

> **⚡ C++ Status: Stub** — `Source/PG_Framework/Public/Player/BPC_StressSystem.h` provides the UCLASS shell, `EStressTier` enum (Calm→Catatonic), `StressTier` variable, and `OnStressTierChanged` dispatcher. **The C++ stub has NO gameplay logic.** **Create a BP child** and build ALL logic: stress accumulation sources, decay during safety, tier transitions, hallucination thresholds. See `docs/developer/cpp-integration-guide.md`.
>
> ---

## Purpose
Tracks the player's psychological state via a stress meter that accumulates from environmental horror, enemy proximity, health deficits, and narrative events. Drives visual distortion, audio hallucinations, gameplay penalties, and narrative branching based on sanity thresholds.

## Dependencies
- **Requires:** `BPC_HealthSystem` (low health triggers stress), `GI_GameFramework` (phase checks)
- **Required By:** `BPC_PlayerController` (shader/audio effects), `WBP_HUD` (stress vignette), `NarrativeManager` (sanity-gated content), `BPC_AdaptiveEnvironment` (world distortion)
- **Engine/Plugin Requirements:** GameplayTags, Timers, Material Parameter Collections (for post-process effects)

## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `ActorComponent` |
| **Class Type** | Blueprint Component |
| **Asset Path** | `Content/Framework/Player/BPC_StressSystem` |
| **Implements Interfaces** | None |

---

## 1. Enums

### `E_StressTier`
| Value | Description |
|-------|-------------|
| `Calm = 0` | No stress effects |
| `Uneasy = 1` | Subtle audio cues, slight FOV shift |
| `Distressed = 2` | Visual noise, heartbeat audio, reduced stamina regen |
| `Panicked = 3` | Heavy distortion, screen shake, movement penalty |
| `Terrified = 4` | Hallucinations, audio distortion, random input inversion |
| `Catatonic = 5` | Brief freeze/stumble, forced slow walk |

### `E_StressSource`
| Value | Description |
|-------|-------------|
| `EnemyProximity = 0` | Nearby hostile presence |
| `EnvironmentalHorror = 1` | Gore, darkness, unsettling areas |
| `HealthDeficit = 2` | Low health triggers fear |
| `NarrativeEvent = 3` | Scripted story beats |
| `Supernatural = 4` | Ghosts, paranormal activity |
| `Isolation = 5` | Long periods alone |
| `TraumaTrigger = 6` | PTSD flashback spots |

---

## 2. Structs

### `S_StressThresholds`
| Field | Type | Description |
|-------|------|-------------|
| `UneasyThreshold` | `Float` | Stress value to enter Uneasy tier |
| `DistressedThreshold` | `Float` | Stress value to enter Distressed |
| `PanickedThreshold` | `Float` | Stress value to enter Panicked |
| `TerrifiedThreshold` | `Float` | Stress value to enter Terrified |
| `CatatonicThreshold` | `Float` | Stress value to enter Catatonic |

### `S_StressSourceData`
| Field | Type | Description |
|-------|------|-------------|
| `SourceType` | `E_StressSource` | Category of source |
| `CurrentIntensity` | `Float` | Current contribution from this source [0..Max] |
| `MaxIntensity` | `Float` | Maximum contribution this source can apply |
| `DecayRate` | `Float` | How fast this source fades when no longer active |
| `SourceTags` | `GameplayTagContainer` | Contextual tags for filtering |
| `SourceIdentifier` | `FName` | Unique name for this specific source instance |

### `S_StressEvent`
| Field | Type | Description |
|-------|------|-------------|
| `Amount` | `Float` | Stress to add or remove |
| `SourceType` | `E_StressSource` | Category of source |
| `Instigator` | `Actor` | What caused the stress change |
| `bIsInstant` | `Boolean` | If true, bypasses buildup curve |
| `EventTags` | `GameplayTagContainer` | Additional context |

---

## 3. Variables

### Configuration (Instance Editable, Expose On Spawn)

| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `MaxStress` | `Float` | `100.0` | `Stress Config` | Maximum stress value |
| `Thresholds` | `S_StressThresholds` | `(15, 30, 50, 75, 90)` | `Stress Config` | Threshold values for each tier |
| `PassiveDecayRate` | `Float` | `2.0` | `Stress Config` | Stress lost per second when in calm area |
| `PanicDecayDelay` | `Float` | `5.0` | `Stress Config` | Seconds at max tier before forced decay begins |
| `bEnableHallucinations` | `Boolean` | `true` | `Stress Config` | Allow visual/audio hallucinations at high stress |
| `bCanGoCatatonic` | `Boolean` | `true` | `Stress Config` | Allow catatonic freeze state |
| `bStressAffectsMovement` | `Boolean` | `true` | `Stress Config` | Apply movement penalties from stress |
| `HealthDeficitMultiplier` | `Float` | `1.5` | `Stress Config` | How much low health amplifies incoming stress |

### Internal (Private / Protected, No Expose)

| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `CurrentStress` | `Float` | `0.0` | `Stress State` | Current accumulated stress |
| `CurrentTier` | `E_StressTier` | `Calm` | `Stress State` | Current stress tier |
| `ActiveSources` | `Map<FName, S_StressSourceData>` | `{}` | `Stress State` | Currently contributing stress sources |
| `StressDecayTimer` | `FTimerHandle` | `-` | `Stress State` | Timer for passive decay tick |
| `bIsInSafeZone` | `Boolean` | `true` | `Stress State` | True when player is in a calm area |
| `LastStressTime` | `Float` | `0.0` | `Stress State` | World time of last stress change |
| `bHallucinationsActive` | `Boolean` | `false` | `Stress State` | Whether hallucinations are currently triggered |
| `TierEntryTimes` | `Map<E_StressTier, float>` | `{}` | `Stress State` | World time when each tier was entered |

### Replicated (if multiplayer)

| Variable | Type | Condition | Description |
|----------|------|-----------|-------------|
| `CurrentStress` | `Float` | `RepNotify` | Replicated for UI and effects |
| `CurrentTier` | `E_StressTier` | `RepNotify` | Replicated tier state |

---

## 4. Functions

### Public Functions

#### `AddStress` → `Float (actual stress added)`
- **Description:** Adds stress from a source. Returns amount actually added.
- **Parameters:**
  | Param | Type | Description |
  |-------|------|-------------|
  | `Event` | `S_StressEvent` | Stress event to apply |
- **Blueprint Authority:** Any
- **Flow:**
  1. If CurrentTier == Catatonic and !Event.bIsInstant: return 0 (immune at max)
  2. If HealthDeficitMultiplier > 1.0 and health < 50%: scale amount
  3. If Event.bIsInstant: CurrentStress += Event.Amount
  4. Else: add/update ActiveSources with Event data, recalculate from sources
  5. Clamp CurrentStress to [0, MaxStress]
  6. Update CurrentTier
  7. Fire OnStressChanged
  8. If tier changed: fire OnStressTierChanged
  9. If tier >= Panicked: start tick for hallucinations
  10. Return delta

#### `RemoveStress` → `Float`
- **Description:** Reduces stress directly or removes a specific source.
- **Parameters:**
  | Param | Type | Description |
  |-------|------|-------------|
  | `Amount` | `Float` | Amount to reduce |
  | `SourceIdentifier` | `FName` | Optional — if set, remove this source entirely |
- **Flow:**
  1. If SourceIdentifier set: remove from ActiveSources, recalculate total
  2. Else: CurrentStress = FMath::Max(0, CurrentStress - Amount)
  3. Update CurrentTier
  4. Fire OnStressChanged
  5. If tier changed: fire OnStressTierChanged

#### `AddStressSource` → `void`
- **Parameters:**
  | Param | Type | Description |
  |-------|------|-------------|
  | `SourceData` | `S_StressSourceData` | Source configuration to add |
- **Flow:**
  1. If SourceData.SourceIdentifier already in ActiveSources: update MaxIntensity and DecayRate
  2. Else: add new entry
  3. Recalculate total stress from all sources

#### `RemoveStressSource` → `void`
- **Parameters:**
  | Param | Type | Description |
  |-------|------|-------------|
  | `SourceIdentifier` | `FName` | Source to remove |
- **Flow:**
  1. Remove from ActiveSources
  2. Recalculate total stress

#### `SetSafeZone` → `void`
- **Parameters:**
  | Param | Type | Description |
  |-------|------|-------------|
  | `bSafe` | `Boolean` | Whether player is in a safe zone |
- **Flow:**
  1. bIsInSafeZone = bSafe
  2. If bSafe: begin passive decay
  3. If !bSafe: stop passive decay
  4. Fire OnSafeZoneChanged

#### `GetStressNormalised` → `Float [0.0 - 1.0]`
- **Flow:** Return CurrentStress / MaxStress

#### `GetTierDuration` → `Float`
- **Parameters:**
  | Param | Type | Description |
  |-------|------|-------------|
  | `Tier` | `E_StressTier` | Which tier to query |
- **Description:** Returns how long the player has been in the given tier.
- **Flow:** If TierEntryTimes contains Tier: return CurrentWorldTime - TierEntryTimes[Tier]; else return 0

### Protected / Private Functions

#### `UpdateTier` → `void`
- **Flow:**
  1. OldTier = CurrentTier
  2. Check CurrentStress against Thresholds in descending order
  3. NewTier = matching tier (highest threshold exceeded)
  4. If NewTier != OldTier:
     - CurrentTier = NewTier
     - Record TierEntryTimes[NewTier] = CurrentWorldTime
     - Fire OnStressTierChanged

#### `RecalculateFromSources` → `void`
- **Flow:**
  1. Total = 0.0
  2. For each ActiveSource: Total += Source.CurrentIntensity
  3. CurrentStress = FMath::Clamp(Total, 0, MaxStress)
  4. UpdateTier
  5. Fire OnStressChanged

#### `PassiveDecayTick` → `void`
- **Flow:**
  1. If bIsInSafeZone or CurrentTier < Distressed or CurrentStress <= 0: return
  2. DecayAmount = PassiveDecayRate * 0.1
  3. CurrentStress = FMath::Max(0, CurrentStress - DecayAmount)
  4. Fire OnStressChanged

#### `HandleHallucinationCheck` → `void`
- **Flow:**
  1. If CurrentTier < Terrified or !bEnableHallucinations:
     - If bHallucinationsActive: StopHallucinations
     - return
  2. Random chance each tick (e.g., 5% per second at Terrified)
  3. If roll succeeds: trigger random hallucination event
  4. Fire OnHallucinationTriggered

---

## 5. Event Dispatchers

| Dispatcher | Parameters | Bind Access | Description |
|------------|-----------|-------------|-------------|
| `OnStressChanged` | `float OldStress`, `float NewStress` | `Public` | Fired on any stress change |
| `OnStressTierChanged` | `E_StressTier OldTier`, `E_StressTier NewTier` | `Public` | Fired when stress tier changes |
| `OnStressSourceAdded` | `FName SourceIdentifier`, `E_StressSource SourceType` | `Public` | Fired when a new stress source is added |
| `OnStressSourceRemoved` | `FName SourceIdentifier` | `Public` | Fired when a stress source is removed |
| `OnSafeZoneChanged` | `bool bIsSafe` | `Public` | Fired when entering/leaving safe zone |
| `OnHallucinationTriggered` | `E_HallucinationType Type` | `Public` | Fired when a hallucination event occurs |
| `OnCatatonicStateEntered` | `none` | `Public` | Fired when entering Catatonic tier |
| `OnStressRecovered` | `none` | `Public` | Fired when stress drops back to Calm |

---

## 6. Overridden Events / Custom Events

### Event: `BeginPlay`
- **Description:** Initialises stress system, binds to health component if available.
- **Flow:**
  1. CurrentStress = 0.0
  2. CurrentTier = Calm
  3. Find BPC_HealthSystem on owner
  4. If found: bind OnDamageTaken to a handler that adds stress for damage taken
  5. Start passive decay timer (interval 0.1s)

### Custom Event: `OnDamageTakenHandler`
- **Parameters:** `S_DamageEvent DamageEvent`, `float EffectiveDamage`
- **Description:** Stress response to taking damage.
- **Flow:**
  1. StressAmount = EffectiveDamage * 0.5
  2. If DamageType == Fear: StressAmount *= 2.0
  3. Build S_StressEvent with SourceType = HealthDeficit
  4. Call AddStress

### Custom Event: `ForcePanic`
- **Parameters:** `float Duration`
- **Description:** Forcibly raises stress to Panicked tier for a duration, used by narrative events.
- **Flow:**
  1. CurrentStress = Thresholds.PanickedThreshold
  2. UpdateTier
  3. Start timer for Duration
  4. On timer end: gradually return stress to previous level

---

## 7. Blueprint Graph Logic Flow

```mermaid
flowchart TD
    A[AddStress called] --> B{CurrentTier == Catatonic?}
    B -->|Yes| C[Return 0]
    B -->|No| D[Apply HealthDeficitMultiplier]
    D --> E[Update ActiveSources map]
    E --> F[Recalculate total stress]
    F --> G[Clamp to 0..MaxStress]
    G --> H{NewTier == OldTier?}
    H -->|No| I[Update CurrentTier]
    I --> J[Record tier entry time]
    J --> K[Fire OnStressTierChanged]
    H -->|Yes| L[Skip tier update]
    K --> M{Tier >= Panicked?}
    M -->|Yes| N[Start hallucination ticker]
    M -->|No| O{Stress dropped below Distressed?}
    O -->|Yes| P[Stop hallucinations]
    O -->|No| Q[Continue passive decay]
    N --> R[Fire OnStressChanged]
    P --> R
    Q --> R
```

---

## 8. Communication Matrix

| Who Talks | How | What Is Sent |
|-----------|-----|-------------|
| `BPC_StressSystem` | `Dispatcher` | `OnStressTierChanged` -> `PC_PlayerController` (post-process), `ABP_GASP` (anim), `WBP_HUD` (vignette) |
| `BPC_StressSystem` | `Dispatcher` | `OnHallucinationTriggered` -> `BP_HallucinationManager`, `BP_AudioManager` |
| `BPC_StressSystem` | `Dispatcher` | `OnStressChanged` -> `WBP_HUD` (stress bar) |
| `BPC_StressSystem` | `Dispatcher` | `OnCatatonicStateEntered` -> `BPC_PlayerController` (input lockdown) |
| `External (Enemies)` | `Direct` | Calls `AddStressSource` on detection |
| `External (BP_DeathZone)` | `Direct` | Calls `AddStress` via interface |
| `BPC_HealthSystem` | `Listener` | Binds to `OnDamageTaken` for health-deficit stress |
| `BPC_StressSystem` | `Dispatcher` | `OnSafeZoneChanged` -> `BPC_AdaptiveAtmosphere` |

---

## 9. Validation / Testing Checklist

- [ ] Stress accumulates from multiple sources and sums correctly
- [ ] Stress decays passively when in safe zone or above Distressed
- [ ] Tier transitions fire exactly once per threshold crossing
- [ ] Catatonic tier blocks further stress accumulation
- [ ] Hallucinations trigger randomly at Terrified tier and stop below it
- [ ] Stress sources with identifiers are properly tracked and removed
- [ ] Edge case: Stress added while at MaxStress clamps and does not overflow
- [ ] Edge case: RemoveStress with source identifier removes only that source
- [ ] Edge case: ForcePanic temporarily locks tier and restores after duration
- [ ] All timers clean up on component destroy

---

## 10. Reuse Notes

- Can be used on NPCs for a simplified "morale" system (remove hallucination features).
- Stress tiers can gate narrative content — questgivers behave differently based on player stress.
- Hallucination types can be configured via Data Asset (DA_HallucinationConfig).
- For multiplayer: Stress is server-authoritative; clients receive tier changes for local effects.

---

## 11. Multiplayer Networking (Expanded)

### Server RPCs
| RPC | Direction | Description |
|-----|-----------|-------------|
| `Server_AddStress` | Client→Server | Client reports stress event. Server validates source, applies. |
| `Server_AddStressSource` | Client→Server | Client enters enemy proximity zone. Server adds/updates source. |
| `Server_RemoveStressSource` | Client→Server | Client leaves enemy zone. Server removes source. |
| `Server_SetSafeZone` | Client→Server | Client reports safe zone status. Server validates, applies decay. |

### Authority
- **Server** calculates total stress from all sources, updates tier, replicates.
- **Client** plays local hallucination/audio effects based on replicated tier.
- `ForcePanic()` is server-only (narrative system calls it directly on server).

### Client Prediction
- Stress bar: predicted on client, corrected by `OnRep_CurrentStress`.
- Hallucinations: randomly triggered by client based on replicated tier (cosmetic only).
- Safe zone: server-authoritative; clients see OnSafeZoneChanged dispatcher.
- The HealthDeficitMultiplier creates a death spiral feel — use carefully for horror pacing.
- Safe zones double as narrative checkpoints and stress recovery areas.

---

*Blueprint Spec: Stress/Sanity System. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.*