Files
UE5-Modular-Game-Framework/docs/blueprints/02-player/10_BPC_StressSystem.md
Lefteris Notas 411edea8ce add blueprints
2026-05-19 13:22:27 +03:00

345 lines
14 KiB
Markdown

# 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<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.
- 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.*