# 10 — Stress / Sanity System (`BPC_StressSystem`) ## 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` | `{}` | `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` | `{}` | `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. - 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.*