371 lines
16 KiB
Markdown
371 lines
16 KiB
Markdown
# 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.* |