add blueprints

This commit is contained in:
Lefteris Notas
2026-05-19 13:22:27 +03:00
parent f71bc678b2
commit 411edea8ce
138 changed files with 23330 additions and 0 deletions

View File

@@ -0,0 +1,340 @@
# 08 — Health System (`BPC_HealthSystem`)
## Purpose
Manages the player's health pool, damage application with type-based resistance, health regeneration, death state, and critical-health events. Serves as the central survivability component on the player character.
## Dependencies
- **Requires:** `FL_GameUtilities` (for safe interface casts), `I_Damageable` (interface on the owner), `GI_GameFramework` (for GamePhase queries)
- **Required By:** `BPC_StressSystem` (health below threshold triggers fear events), `BPC_PlayerMetricsTracker` (death tracking), `WBP_HUD` (health bar), `AI_EnemyController` (player health as awareness trigger), `BPC_InteractionDetector` (low-health checks)
- **Engine/Plugin Requirements:** GameplayTags, DeveloperSettings
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `ActorComponent` |
| **Class Type** | Blueprint Component |
| **Asset Path** | `Content/Framework/Player/BPC_HealthSystem` |
| **Implements Interfaces** | `I_Damageable` |
---
## 1. Enums
### `E_DamageType`
| Value | Description |
|-------|-------------|
| `Physical = 0` | Bullets, blunt force, falls |
| `Arcane = 1` | Magic, curse, psychic damage |
| `Fire = 2` | Burning, environmental heat |
| `Poison = 3` | Toxins, venom, chemical |
| `Fear = 4` | Psychological damage (stress overflow) |
| `Environmental = 5` | Traps, crushing, drowning |
| `True = 6` | Bypasses all resistances, no reduction |
### `E_DeathState`
| Value | Description |
|-------|-------------|
| `Alive = 0` | Normal gameplay |
| `Dying = 1` | Downed state, waiting for recovery or final death |
| `Dead = 2` | Character is dead, no further actions possible |
| `PermaDeath = 3` | Save-scrubbing locked, forced reload |
---
## 2. Structs
### `S_DamageEvent`
| Field | Type | Description |
|-------|------|-------------|
| `BaseAmount` | `Float` | Raw damage before resistance |
| `DamageType` | `E_DamageType` | Category of damage |
| `Instigator` | `Actor` | Who caused the damage |
| `HitLocation` | `Vector` | World location of impact |
| `DamageTags` | `GameplayTagContainer` | Additional contextual tags |
| `bIsCriticalHit` | `Boolean` | True if this is a headshot / weak-point hit |
| `SourceDescription` | `Text` | Human-readable source (for log / UI) |
### `S_DamageResistance`
| Field | Type | Description |
|-------|------|-------------|
| `DamageType` | `E_DamageType` | Type this resistance applies to |
| `Multiplier` | `Float` | 1.0 = normal, 0.5 = half damage, 2.0 = double |
| `bIsStackable` | `Boolean` | Can this resistance stack with others? |
---
## 3. Variables
### Configuration (Instance Editable, Expose On Spawn)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `MaxHealth` | `Float` | `100.0` | `Health Config` | Maximum hit points |
| `bRegenEnabled` | `Boolean` | `true` | `Health Config` | Allow passive health regeneration |
| `RegenDelay` | `Float` | `3.0` | `Health Config` | Seconds after last damage before regen starts |
| `RegenRate` | `Float` | `2.0` | `Health Config` | HP recovered per second during regen |
| `RegenDelayUntil` | `Float` | `0.0` | `Health Config` | World time when regen can resume |
| `CriticalHealthPercent` | `Float` | `0.25` | `Health Config` | Below this fraction of MaxHealth, fire critical event |
| `DefaultResistances` | `Array<S_DamageResistance>` | `[]` | `Health Config` | Base damage resistances for this character |
| `bEnableFriendlyFire` | `Boolean` | `false` | `Health Config` | Can the player damage themselves? |
### Internal (Private / Protected, No Expose)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `CurrentHealth` | `Float` | `100.0` | `Health State` | Current HP, clamped [0..MaxHealth] |
| `DeathState` | `E_DeathState` | `Alive` | `Health State` | Current death state |
| `ActiveDamageOverTime` | `Map<FName, FTimerHandle>` | `{}` | `Health State` | Active DoT timers keyed by source name |
| `InvincibilityTimer` | `FTimerHandle` | `-` | `Health State` | Timer handle for temporary invincibility |
| `RegenTimerHandle` | `FTimerHandle` | `-` | `Health State` | Timer handle for regen tick |
| `bIsInvincible` | `Boolean` | `false` | `Health State` | True during invincibility window |
| `LastDamageInstigator` | `Actor` | `None` | `Health State` | Most recent damage source (for kill credit) |
| `LastDamageTime` | `Float` | `0.0` | `Health State` | World time of last damage taken |
### Replicated (if multiplayer)
| Variable | Type | Condition | Description |
|----------|------|-----------|-------------|
| `CurrentHealth` | `Float` | `RepNotify` | Replicated with OnRep handler for UI updates |
| `DeathState` | `E_DeathState` | `RepNotify` | Replicated death state |
---
## 4. Functions
### Public Functions
#### `ApplyDamage` → `S_DamageResult (custom struct)`
- **Description:** Applies damage to the health pool after resistance calculation. Returns effective damage and any side effects.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `DamageEvent` | `S_DamageEvent` | The damage event to process |
- **Blueprint Authority:** Server (if MP), Any (single-player)
- **Flow:**
1. Early-out if DeathState is Dead or bIsInvincible
2. Calculate effective damage from BaseAmount * type resistances
3. If damage > 0: reset RegenDelay timer, update CurrentHealth
4. Fire OnDamageTaken dispatcher
5. If CurrentHealth <= 0: call `HandleDeath`
6. If CurrentHealth <= CriticalHealthPercent: fire OnHealthCritical
7. Return damage result struct
#### `ApplyHealing` → `Float (effective healing)`
- **Description:** Adds health up to MaxHealth. Returns the amount actually healed.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `HealAmount` | `Float` | Raw healing value |
| `HealTags` | `GameplayTagContainer` | Contextual tags |
- **Blueprint Authority:** Server (if MP), Any (single-player)
- **Flow:**
1. Early-out if DeathState is Dead
2. Clamp: `CurrentHealth = FMath::Min(MaxHealth, CurrentHealth + HealAmount)`
3. Fire OnHealthChanged
4. Return actual heal amount
#### `GetHealthNormalised` → `Float [0.0 - 1.0]`
- **Description:** Returns NormalisedCurrentHealth for UI widgets.
- **Flow:** `Return CurrentHealth / MaxHealth`
#### `SetMaxHealth` → `void`
- **Description:** Changes MaxHealth and optionally heals to match new ratio or clamps current.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `NewMax` | `Float` | New maximum health value |
| `bMaintainRatio` | `Boolean` | If true, scale CurrentHealth to same ratio |
- **Flow:**
1. Clamp NewMax to >= 1.0
2. If bMaintainRatio: `CurrentHealth = (CurrentHealth / MaxHealth) * NewMax`
3. Else: `CurrentHealth = FMath::Min(CurrentHealth, NewMax)`
4. Set MaxHealth = NewMax
5. Fire OnHealthChanged
#### `KillInstant` → `void`
- **Description:** Bypasses all resistances, immediately sets health to 0 and triggers death.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `Instigator` | `Actor` | Who caused the kill |
| `DeathReason` | `FText` | Reason displayed on death screen |
- **Blueprint Authority:** Server (if MP), Any (single-player)
- **Flow:**
1. Set CurrentHealth = 0
2. Set DeathState = Dying
3. Fire OnHealthChanged with CurrentHealth = 0
4. Call HandleDeath with bInstant = true
#### `ApplyDamageOverTime` → `void`
- **Description:** Applies periodic damage with tick interval and total duration.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `DamagePerTick` | `Float` | Damage applied each interval |
| `TickInterval` | `Float` | Seconds between ticks |
| `TotalDuration` | `Float` | Total seconds the DoT lasts |
| `DamageType` | `E_DamageType` | Type of each damage tick |
| `SourceName` | `FName` | Unique name to prevent stacking same DoT |
| `Instigator` | `Actor` | Who applied the DoT |
- **Flow:**
1. If SourceName already exists in ActiveDamageOverTime, clear existing timer
2. Create a Timer by Event with loop = TickInterval
3. On each tick: build S_DamageEvent, call ApplyDamage
4. After TotalDuration: clear timer, remove from ActiveDamageOverTime
#### `IsAlive` → `Boolean`
- **Flow:** `Return DeathState == Alive`
#### `AddResistance` → `void`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `NewResistance` | `S_DamageResistance` | Resistance to add or update |
- **Flow:** If resistance for this DamageType exists, update Multiplier; if not, add to array.
#### `RemoveResistance` → `void`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `DamageType` | `E_DamageType` | Type to remove |
- **Flow:** Remove all entries matching this DamageType from resistance array.
### Protected / Private Functions
#### `HandleDeath` → `void`
- **Description:** Internal death processing. Sets state, fires dispatchers, notifies GameMode.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `bInstant` | `Boolean` | If true, skip dying state go straight to dead |
- **Flow:**
1. Set DeathState = (bInstant ? Dead : Dying)
2. Clear all timers (regen, DoT, invincibility)
3. Fire OnDeath with DeathState
4. Call `I_Damageable::OnOwnerDied` on owner if interface exists
5. Get `GM_CoreGameMode` via `GetWorld()->GetAuthGameMode()`
6. Call `GM_CoreGameMode::HandlePlayerDead(LastDamageInstigator)`
#### `RegenTick` → `void`
- **Description:** Timer callback — applies one tick of regeneration.
- **Flow:**
1. If CurrentHealth >= MaxHealth: stop regen timer, return
2. `CurrentHealth = FMath::Min(MaxHealth, CurrentHealth + RegenRate * TickInterval)`
3. Fire OnHealthChanged
4. If CurrentHealth >= MaxHealth: stop regen timer
---
## 5. Event Dispatchers
| Dispatcher | Parameters | Bind Access | Description |
|------------|-----------|-------------|-------------|
| `OnHealthChanged` | `float OldHealth`, `float NewHealth`, `float Delta` | `Public` | Fired when health changes by any amount |
| `OnDamageTaken` | `S_DamageEvent DamageEvent`, `float EffectiveDamage` | `Public` | Fired each time damage is applied |
| `OnHealingReceived` | `float HealAmount`, `GameplayTagContainer HealTags` | `Public` | Fired each time healing is applied |
| `OnHealthCritical` | `float CurrentHealth`, `float MaxHealth` | `Public` | Fired when health drops below critical threshold |
| `OnDeath` | `E_DeathState DeathState`, `Actor Instigator` | `Public` | Fired when character dies or enters dying state |
| `OnDeathStateChanged` | `E_DeathState OldState`, `E_DeathState NewState` | `Public` | Fired on any death state transition |
| `OnInvincibilityChanged` | `bool bIsInvincible` | `Public` | Fired when invincibility state toggles |
---
## 6. Overridden Events / Custom Events
### Event: `BeginPlay`
- **Description:** Initializes health, loads saved health if persisting, starts regen timer if enabled.
- **Flow:**
1. Set CurrentHealth = MaxHealth
2. Check if owner implements `I_Persistable`
3. If yes, try to load saved health value
4. If bRegenEnabled: start regen timer with initial delay = 0
5. Bind to any relevant GamePhase change dispatchers
### Event: `OnComponentDestroyed`
- **Description:** Clean up all active timers.
- **Flow:**
1. Clear all TimerHandles (Invincibility, Regen, all DoT timers)
2. Clear ActiveDamageOverTime map
### Custom Event: `ApplyTemporaryInvincibility`
- **Parameters:** `float Duration`
- **Description:** Grants brief invincibility, often used after respawn or dodge.
- **Flow:**
1. Set bIsInvincible = true
2. Fire OnInvincibilityChanged(true)
3. Start timer for Duration
4. On timer end: set bIsInvincible = false, fire OnInvincibilityChanged(false)
---
## 7. Blueprint Graph Logic Flow
```mermaid
flowchart TD
A[ApplyDamage called] --> B{DeathState == Dead?}
B -->|Yes| C[Return 0 damage]
B -->|No| D{bIsInvincible?}
D -->|Yes| C
D -->|No| E[Calculate resistance multiplier]
E --> F[EffectiveDamage = Base * Multiplier]
F --> G[CurrentHealth -= EffectiveDamage]
G --> H[Reset RegenDelay timer]
H --> I[Fire OnDamageTaken]
I --> J{CurrentHealth <= 0?}
J -->|Yes| K[HandleDeath]
J -->|No| L{CurrentHealth <= CriticalThreshold?}
L -->|Yes| M[Fire OnHealthCritical]
L -->|No| N[Fire OnHealthChanged]
K --> O[Fire OnDeath dispatcher]
K --> P[Notify GameMode: HandlePlayerDead]
K --> Q[Clear all timers]
M --> N
```
---
## 8. Communication Matrix
| Who Talks | How | What Is Sent |
|-----------|-----|-------------|
| `BPC_HealthSystem` | `Dispatcher` | `OnHealthChanged` -> `WBP_HUD`, `BPC_StressSystem` |
| `BPC_HealthSystem` | `Dispatcher` | `OnDeath` -> `GM_CoreGameMode`, `SS_SaveSystem`, `BPC_PlayerMetricsTracker` |
| `BPC_HealthSystem` | `Dispatcher` | `OnDamageTaken` -> `BPC_StressSystem`, `BPC_AdaptiveEnvironment` |
| `BPC_HealthSystem` | `Interface` | `I_Damageable.Execute_ApplyDamage` route |
| `BPC_HealthSystem` | `Interface` | `I_Persistable` for save/load of CurrentHealth |
| `External (Weapons, Traps)` | `Interface` | `I_Damageable -> ApplyDamage` |
| `BPC_HealthSystem` | `Direct` | `GM_CoreGameMode.HandlePlayerDead` on death |
| `BPC_HealthSystem` | `Subsystem` | `GI_GameFramework.GetGamePhase` for phase-restricted actions |
---
## 9. Validation / Testing Checklist
- [ ] ApplyDamage with all E_DamageType values produces correct resistance-modified damage
- [ ] Health never drops below 0 or exceeds MaxHealth
- [ ] Regeneration does not start while RegenDelay timer is active
- [ ] Regeneration stops when health reaches MaxHealth
- [ ] Death sets DeathState properly and fires OnDeath exactly once
- [ ] Temporary invincibility blocks all damage for its duration
- [ ] Damage-over-time correctly applies ticks and cleans up timer on death
- [ ] Critical health event fires only when crossing below threshold from above
- [ ] Edge case: ApplyHealing on a dead character returns 0 with no dispatchers
- [ ] Edge case: ApplyDamage with negative BaseAmount is treated as 0
- [ ] Edge case: Multiple simultaneous DoT sources with same SourceName do not stack
- [ ] All dispatchers are cleaned up on component destroy
---
## 10. Reuse Notes
- This component can be used on AI characters and enemy pawns by exposing the same configuration variables.
- For enemies, consider disabling regen (bRegenEnabled = false) unless a specific healing mechanic exists.
- The resistance system supports temporary buffs via AddResistance / RemoveResistance at runtime.
- If implementing a downed / bleeding state, extend E_DeathState with custom values and add a recovery timer check in HandleDeath.
- Damage numbers displayed as floating text should bind to OnDamageTaken.
- For multiplayer: all ApplyDamage logic runs on Server; clients receive repnotified CurrentHealth and fire local cosmetic dispatchers only.
---
*Blueprint Spec: Health System. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.*

View File

@@ -0,0 +1,333 @@
# 09 — Stamina System (`BPC_StaminaSystem`)
## Purpose
Manages the player's stamina pool — drain during sprinting, jumping, and special actions; regeneration during idle/walking. Prevents action spamming by enforcing minimum stamina thresholds. Drives exhaustion VFX and audio cues.
## Dependencies
- **Requires:** `GI_GameFramework` (phase checks), `FL_GameUtilities` (tag queries)
- **Required By:** `BPC_MovementStateSystem` (stamina affects max speed), `WBP_HUD` (stamina bar), `BPC_PlayerMetricsTracker` (exhaustion tracking)
- **Engine/Plugin Requirements:** GameplayTags, Timers
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `ActorComponent` |
| **Class Type** | Blueprint Component |
| **Asset Path** | `Content/Framework/Player/BPC_StaminaSystem` |
| **Implements Interfaces** | None |
---
## 1. Enums
### `E_ExhaustionState`
| Value | Description |
|-------|-------------|
| `Normal = 0` | Full stamina operation |
| `Low = 1` | Below LowThreshold, heavy breathing, slightly reduced regen delay |
| `Exhausted = 2` | Below ExhaustedThreshold, cannot sprint, slow regen |
### `E_StaminaActionType`
| Value | Description |
|-------|-------------|
| `Sprint = 0` | Continuous drain while sprinting |
| `Jump = 1` | One-time drain on jump |
| `Climb = 2` | Continuous drain while climbing |
| `SpecialAttack = 3` | One-time drain for strong melee |
| `Dodge = 4` | One-time drain for dodging |
| `Environment = 5` | Drain from external sources (cold, poison) |
---
## 2. Structs
### `S_StaminaDrainRate`
| Field | Type | Description |
|-------|------|-------------|
| `ActionType` | `E_StaminaActionType` | Which action this rate applies to |
| `DrainPerSecond` | `Float` | Stamina drained per second (continuous) |
| `DrainFlat` | `Float` | Stamina drained once per use (one-shot) |
| `MinStamina` | `Float` | Minimum stamina required to perform this action |
| `CooldownAfterUse` | `Float` | Seconds before this action can be used again |
### `S_StaminaRegenSettings`
| Field | Type | Description |
|-------|------|-------------|
| `RegenRate` | `Float` | Stamina recovered per second |
| `RegenDelay` | `Float` | Seconds after last drain before regen begins |
| `ExhaustedRegenRate` | `Float` | Slower recovery while exhausted |
| `ExhaustedRegenDelay` | `Float` | Longer delay while exhausted |
| `RegenBlockedWhileMoving` | `Boolean` | If true, regen pauses while moving above walk speed |
---
## 3. Variables
### Configuration (Instance Editable, Expose On Spawn)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `MaxStamina` | `Float` | `100.0` | `Stamina Config` | Maximum stamina pool |
| `DefaultDrainRates` | `Array<S_StaminaDrainRate>` | `[]` | `Stamina Config` | Base drain rates for each action type |
| `RegenSettings` | `S_StaminaRegenSettings` | `(15.0, 2.0, 5.0, 4.0, true)` | `Stamina Config` | Regeneration behaviour |
| `LowThreshold` | `Float` | `0.30` | `Stamina Config` | Fraction of MaxStamina for Low state |
| `ExhaustedThreshold` | `Float` | `0.10` | `Stamina Config` | Fraction of MaxStamina for Exhausted state |
| `bCanExhaust` | `Boolean` | `true` | `Stamina Config` | Allow exhaustion state |
### Internal (Private / Protected, No Expose)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `CurrentStamina` | `Float` | `100.0` | `Stamina State` | Current stamina pool |
| `ExhaustionState` | `E_ExhaustionState` | `Normal` | `Stamina State` | Current exhaustion tier |
| `ActiveDrains` | `Map<E_StaminaActionType, bool>` | `{}` | `Stamina State` | Which drains are currently active |
| `RegenTimerHandle` | `FTimerHandle` | `-` | `Stamina State` | Timer for regen ticks |
| `DrainTimerHandle` | `FTimerHandle` | `-` | `Stamina State` | Timer for continuous drain ticks |
| `LastDrainTime` | `Float` | `0.0` | `Stamina State` | World time of last stamina drain |
| `bRegenBlocked` | `Boolean` | `false` | `Stamina State` | True while external block is active |
| `ActionCooldownTimers` | `Map<E_StaminaActionType, FTimerHandle>` | `{}` | `Stamina State` | Per-action cooldown timers |
### Replicated (if multiplayer)
| Variable | Type | Condition | Description |
|----------|------|-----------|-------------|
| `CurrentStamina` | `Float` | `RepNotify` | Replicated with OnRep handler for UI updates |
| `ExhaustionState` | `E_ExhaustionState` | `RepNotify` | Replicated exhaustion tier |
---
## 4. Functions
### Public Functions
#### `DrainStamina` → `Boolean (success)`
- **Description:** Attempts to drain stamina for a specific action. Returns false if insufficient stamina or cooldown active.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `ActionType` | `E_StaminaActionType` | Which action is being performed |
| `OverrideAmount` | `Float` | If > 0, use this instead of the DrainRate config value |
- **Blueprint Authority:** Server (if MP), Any (single-player)
- **Flow:**
1. If ActionCooldownTimers contains ActionType and timer active: return false
2. Look up S_StaminaDrainRate for ActionType
3. RequiredStamina = DrainRate.MinStamina
4. If CurrentStamina < RequiredStamina: return false
5. DrainAmount = OverrideAmount > 0 ? OverrideAmount : DrainRate.DrainFlat
6. CurrentStamina = FMath::Max(0, CurrentStamina - DrainAmount)
7. Start cooldown timer for DrainRate.CooldownAfterUse
8. Update ExhaustionState
9. Reset regen delay, stop regen if running
10. Fire OnStaminaDrained
11. If ActionType is continuous: add to ActiveDrains
12. Fire OnStaminaChanged
13. Return true
#### `StartContinuousDrain` → `void`
- **Description:** Begins draining stamina per second for a continuous action.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `ActionType` | `E_StaminaActionType` | Sprint, Climb, etc. |
- **Flow:**
1. Add ActionType to ActiveDrains = true
2. If DrainTimerHandle not active: start timer with interval 0.1 seconds
3. Timer callback: apply (DrainRate.DrainPerSecond * 0.1) to CurrentStamina each tick
4. If CurrentStamina <= 0: stop continuous drain, fire OnExhausted
#### `StopContinuousDrain` → `void`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `ActionType` | `E_StaminaActionType` | Action to stop draining for |
- **Flow:**
1. Set ActiveDrains[ActionType] = false
2. If no active drains remain: clear DrainTimerHandle
3. Start regen delay timer
#### `RestoreStamina` → `Float (actual restored)`
- **Description:** Adds stamina up to MaxStamina.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `Amount` | `Float` | Stamina to restore |
| `Tags` | `GameplayTagContainer` | Context tags (e.g. Potion, Rest) |
- **Flow:**
1. Previous = CurrentStamina
2. CurrentStamina = FMath::Min(MaxStamina, CurrentStamina + Amount)
3. Update ExhaustionState
4. Fire OnStaminaRestored
5. Fire OnStaminaChanged
6. Return CurrentStamina - Previous
#### `GetStaminaNormalised` → `Float [0.0 - 1.0]`
- **Flow:** Return CurrentStamina / MaxStamina
#### `CanAffordAction` → `Boolean`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `ActionType` | `E_StaminaActionType` | Action to check |
- **Flow:**
1. Look up MinStamina for this ActionType
2. Return CurrentStamina >= MinStamina AND no active cooldown
#### `SetMaxStamina` → `void`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `NewMax` | `Float` | New maximum value |
| `bMaintainRatio` | `Boolean` | Scale current stamina to maintain ratio |
- **Flow:**
1. If bMaintainRatio: CurrentStamina = (CurrentStamina / MaxStamina) * NewMax
2. Else: CurrentStamina = FMath::Min(CurrentStamina, NewMax)
3. MaxStamina = NewMax
4. Update ExhaustionState
5. Fire OnStaminaChanged
#### `BlockRegen` → `void`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `Duration` | `Float` | Seconds to block regen |
- **Flow:**
1. Set bRegenBlocked = true
2. Start timer for Duration
3. On timer end: bRegenBlocked = false
### Protected / Private Functions
#### `UpdateExhaustionState` → `void`
- **Flow:**
1. Normalised = CurrentStamina / MaxStamina
2. OldState = ExhaustionState
3. If Normalised <= ExhaustedThreshold: NewState = Exhausted
4. Else if Normalised <= LowThreshold: NewState = Low
5. Else: NewState = Normal
6. If NewState != OldState:
- ExhaustionState = NewState
- Fire OnExhaustionStateChanged
- If NewState == Exhausted: fire OnExhausted
#### `RegenTick` → `void`
- **Flow:**
1. If bRegenBlocked: return
2. Rate = ExhaustionState == Exhausted ? RegenSettings.ExhaustedRegenRate : RegenSettings.RegenRate
3. CurrentStamina = FMath::Min(MaxStamina, CurrentStamina + Rate * 0.1)
4. Fire OnStaminaChanged
5. If CurrentStamina >= MaxStamina: stop regen timer, fire OnStaminaFullyRestored
#### `OnMovementStateChanged (listener)` → `void`
- **Flow:**
1. Get movement mode from character
2. If RegenSettings.RegenBlockedWhileMoving and movement speed > walk:
- Pause regen timer
3. Else: resume regen timer if conditions met
---
## 5. Event Dispatchers
| Dispatcher | Parameters | Bind Access | Description |
|------------|-----------|-------------|-------------|
| `OnStaminaChanged` | `float OldStamina`, `float NewStamina`, `float Delta` | `Public` | Fired on any stamina change |
| `OnStaminaDrained` | `E_StaminaActionType ActionType`, `float Amount` | `Public` | Fired when stamina is consumed |
| `OnStaminaRestored` | `float Amount`, `GameplayTagContainer Tags` | `Public` | Fired when stamina is gained |
| `OnStaminaFullyRestored` | `none` | `Public` | Fired when stamina reaches MaxStamina |
| `OnExhaustionStateChanged` | `E_ExhaustionState OldState`, `E_ExhaustionState NewState` | `Public` | Fired on any exhaustion tier change |
| `OnExhausted` | `none` | `Public` | Fired when entering Exhausted state |
| `OnActionCooldownStarted` | `E_StaminaActionType ActionType`, `float Cooldown` | `Public` | Fired when a per-action cooldown begins |
| `OnActionCooldownEnded` | `E_StaminaActionType ActionType` | `Public` | Fired when a per-action cooldown expires |
---
## 6. Overridden Events / Custom Events
### Event: `BeginPlay`
- **Description:** Initialises stamina, starts regen if enabled, binds to movement state changes.
- **Flow:**
1. CurrentStamina = MaxStamina
2. Start regen timer with initial delay = 0
3. Find BPC_MovementStateSystem on owner, bind to its OnMovementStateChanged dispatcher
4. Initialise ExhaustionState = Normal
### Event: `OnComponentDestroyed`
- **Description:** Clean up all timers.
- **Flow:**
1. Clear RegenTimerHandle, DrainTimerHandle
2. Clear all ActionCooldownTimers
---
## 7. Blueprint Graph Logic Flow
```mermaid
flowchart TD
A[DrainStamina called] --> B{CanAffordAction?}
B -->|No| C[Return false]
B -->|Yes| D[Apply drain amount]
D --> E[Update CurrentStamina]
E --> F{ExhaustedThreshold crossed?}
F -->|Yes| G[Set Exhausted state]
F -->|No| H{LowThreshold crossed?}
H -->|Yes| I[Set Low state]
H -->|No| J[Keep Normal state]
G --> K[Fire OnExhausted]
K --> L[Stop continuous drains]
I --> M[Fire OnExhaustionStateChanged]
J --> M
L --> M
M --> N[Start regen delay]
N --> O[Fire OnStaminaChanged]
O --> P[Return true]
D --> Q{Continuous action?}
Q -->|Yes| R[Start DrainTimer loop]
Q -->|No| S[Start action cooldown]
R --> O
S --> O
```
---
## 8. Communication Matrix
| Who Talks | How | What Is Sent |
|-----------|-----|-------------|
| `BPC_StaminaSystem` | `Dispatcher` | `OnStaminaChanged` -> `WBP_HUD` (stamina bar update) |
| `BPC_StaminaSystem` | `Dispatcher` | `OnExhausted` -> `BPC_PlayerController` (player feedback), `BPC_AdaptiveEnvironment` |
| `BPC_StaminaSystem` | `Dispatcher` | `OnExhaustionStateChanged` -> `ABP_GASP` (breathing animation) |
| `External (PC_PlayerController)` | `Direct` | Calls `DrainStamina(Sprint)` / `StartContinuousDrain` on input |
| `BPC_StaminaSystem` | `Dispatcher` | `OnStaminaFullyRestored` -> `WBP_HUD` (hide bar) |
| `BPC_StaminaSystem` | `Listener` | Binds to `BPC_MovementStateSystem.OnMovementStateChanged` for regen blocking |
---
## 9. Validation / Testing Checklist
- [ ] DrainStamina deducts correct amount and returns false when stamina is insufficient
- [ ] Continuous drain stops when stamina reaches 0
- [ ] Regen does not start until RegenDelay has passed
- [ ] Exhausted state correctly uses ExhaustedRegenRate and ExhaustedRegenDelay
- [ ] Exhaustion state transitions are one-way downward until full rest
- [ ] Per-action cooldowns prevent spamming dodge/jump
- [ ] BlockRegen pauses all regen for its duration
- [ ] RegenBlockedWhileMoving pauses regen at high speed, resumes when walking
- [ ] Edge case: MaxStamina change with bMaintainRatio=true preserves percentage
- [ ] Edge case: Multiple simultaneous continuous drains stack correctly
- [ ] Edge case: DrainStamina called during active cooldown returns false without state change
- [ ] All timers cleaned up on component destroy
---
## 10. Reuse Notes
- Can be placed on AI characters with simplified config (e.g. only Sprint drain, no Exhaustion).
- For enemy stamina, consider removing the exhaustion mechanic and using only drain/restore.
- The drain rate map supports runtime modification for buffs/debuffs (e.g. Adrenaline reduces sprint cost).
- UI widgets should bind to OnStaminaChanged for smooth bar animation, not tick polling.
- For multiplayer: Server authorises all DrainStamina calls; client predicts and corrects on repnotify.
- Exhaustion state can drive conditional audio: heavy breathing, heart-beat, fatigue voice lines.
---
*Blueprint Spec: Stamina System. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.*

View File

@@ -0,0 +1,345 @@
# 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.*

View File

@@ -0,0 +1,323 @@
# 11 — Movement State System (`BPC_MovementStateSystem`)
## Purpose
Tracks the player's current movement mode and posture state. Acts as a central query point for other systems (animations, stamina, audio, camera) to react to how the player is moving. Does not control movement input — only reports and reacts to state changes.
## Dependencies
- **Requires:** `FL_GameUtilities` (tag queries)
- **Required By:** `ABP_GASP` (animations), `BPC_StaminaSystem` (regen blocking), `BPC_CameraStateLayer` (FOV changes), `BPC_AudioManager` (footstep type), `BPC_InteractionDetector` (range scaling)
- **Engine/Plugin Requirements:** GameplayTags, GASP (ground/animation/strafing platform)
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `ActorComponent` |
| **Class Type** | Blueprint Component |
| **Asset Path** | `Content/Framework/Player/BPC_MovementStateSystem` |
| **Implements Interfaces** | None |
---
## 1. Enums
### `E_PostureState`
| Value | Description |
|-------|-------------|
| `Standing = 0` | Full height, normal locomotion |
| `Crouching = 1` | Reduced height, slower speed |
| `Prone = 2` | Fully prone, crawling (horror immersion) |
| `Sliding = 3` | Transient — while sliding downhill or under obstacles |
| `Climbing = 4` | Ledge or ladder climbing |
| `Vaulting = 5` | Transient — while vaulting over obstacles |
### `E_MovementMode`
| Value | Description |
|-------|-------------|
| `Idle = 0` | Standing still |
| `Walking = 1` | Slow, careful movement |
| `Jogging = 2` | Normal movement speed |
| `Sprinting = 3` | Fast movement, stamina drain |
| `CrouchWalk = 4` | Slow crouch movement |
| `Sneaking = 5` | Ultra-slow, near-silent movement (special) |
### `E_StanceTransition`
| Value | Description |
|-------|-------------|
| `Instant = 0` | Immediate transition (no animation blend) |
| `Smooth = 1` | Smooth blend over TransitionBlendTime |
| `Forced = 2` | External force (knocked down, pushed) |
---
## 2. Structs
### `S_MovementSettings`
| Field | Type | Description |
|-------|------|-------------|
| `MovementMode` | `E_MovementMode` | Which mode this applies to |
| `MaxWalkSpeed` | `Float` | Max speed in this mode (cm/s) |
| `Acceleration` | `Float` | Acceleration rate |
| `Deceleration` | `Float` | Deceleration rate |
| `GroundFriction` | `Float` | Friction modifier |
| `bCanSprint` | `Boolean` | Can the player sprint from this mode? |
| `bCanCrouch` | `Boolean` | Can the player crouch from this mode? |
| `StaminaDrainMultiplier` | `Float` | Multiplier on stamina drain while in this mode |
### `S_FootstepProfile`
| Field | Type | Description |
|-------|------|-------------|
| `SurfaceType` | `EPhysicalSurface` | Surface being stepped on |
| `MovementMode` | `E_MovementMode` | Player's current movement mode |
| `SoundSoft` | `SoundBase` | Footstep sound at low velocity |
| `SoundHard` | `SoundBase` | Footstep sound at high velocity |
| `VelocityThreshold` | `Float` | Speed at which to switch from Soft to Hard |
| `Decal` | `MaterialInterface` | Footprint decal (mud, snow, blood) |
---
## 3. Variables
### Configuration (Instance Editable, Expose On Spawn)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `MovementSettings` | `Map<E_MovementMode, S_MovementSettings>` | `all modes` | `Movement Config` | Speed/accel values per movement mode |
| `TransitionBlendTime` | `Float` | `0.2` | `Movement Config` | Blend duration for smooth stance transitions |
| `bApplyMovementPenalties` | `Boolean` | `true` | `Movement Config` | Allow stress/health to affect movement speed |
| `CrouchHeight` | `Float` | `60.0` | `Movement Config` | Capsule half-height while crouching |
| `ProneHeight` | `Float` | `30.0` | `Movement Config` | Capsule half-height while prone |
| `StandingHeight` | `Float` | `96.0` | `Movement Config` | Capsule half-height while standing |
| `bUseVelocityBasedFootsteps` | `Boolean` | `true` | `Movement Config` | Auto-select footstep sound based on velocity |
| `DefaultFootstepProfile` | `S_FootstepProfile` | `-` | `Movement Config` | Fallback footstep when no surface match |
### Internal (Private / Protected, No Expose)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `CurrentPosture` | `E_PostureState` | `Standing` | `Movement State` | Current posture |
| `PreviousPosture` | `E_PostureState` | `Standing` | `Movement State` | Previous posture (for transition detection) |
| `CurrentMovementMode` | `E_MovementMode` | `Idle` | `Movement State` | Current movement mode |
| `PreviousMovementMode` | `E_MovementMode` | `Idle` | `Movement State` | Previous movement mode |
| `bIsOnGround` | `Boolean` | `true` | `Movement State` | Whether the player is grounded |
| `bIsMoving` | `Boolean` | `false` | `Movement State` | Whether the player is currently moving |
| `CurrentSpeed` | `Float` | `0.0` | `Movement State` | Current character speed (cm/s) |
| `bIsClimbing` | `Boolean` | `false` | `Movement State` | Whether currently climbing |
| `LastValidFloorNormal` | `Vector` | `(0,0,1)` | `Movement State` | Last valid ground normal |
| `CapsuleRef` | `UCapsuleComponent` | `None` | `Movement State` | Cached reference to capsule component |
### Replicated (if multiplayer)
| Variable | Type | Condition | Description |
|----------|------|-----------|-------------|
| `CurrentPosture` | `E_PostureState` | `RepNotify` | Replicated posture state |
| `CurrentMovementMode` | `E_MovementMode` | `RepNotify` | Replicated movement mode |
---
## 4. Functions
### Public Functions
#### `SetMovementMode` → `void`
- **Description:** Sets the current movement mode and applies corresponding movement settings to the CharacterMovementComponent.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `NewMode` | `E_MovementMode` | Desired movement mode |
| `Transition` | `E_StanceTransition` | How to handle the transition |
- **Blueprint Authority:** Server (if MP), Any (single-player)
- **Flow:**
1. If NewMode == CurrentMovementMode: return
2. PreviousMovementMode = CurrentMovementMode
3. CurrentMovementMode = NewMode
4. Apply MovementSettings[NewMode] to CharacterMovementComponent
5. Fire OnMovementModeChanged
6. If transition is Forced: apply impulse or knockback
7. Notify ABP_GASP via interface or direct cast
#### `SetPosture` → `void`
- **Description:** Sets the current posture and adjusts capsule size accordingly.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `NewPosture` | `E_PostureState` | Desired posture |
| `Transition` | `E_StanceTransition` | How to handle the transition |
- **Flow:**
1. If NewPosture == CurrentPosture: return
2. If NewPosture == Prone and CurrentPosture == Standing: must go through Crouching first
3. PreviousPosture = CurrentPosture
4. CurrentPosture = NewPosture
5. Update capsule height based on NewPosture
6. Fire OnPostureChanged
7. Notify ABP_GASP
#### `GetCurrentSpeedNormalised` → `Float [0.0 - 1.0]`
- **Description:** Returns speed as fraction of CurrentMovementMode's MaxWalkSpeed.
- **Flow:** Return CurrentSpeed / MovementSettings[CurrentMovementMode].MaxWalkSpeed
#### `CanTransitionToPosture` → `Boolean`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `TargetPosture` | `E_PostureState` | Desired posture |
- **Flow:**
1. Check ceiling clearance for upright transitions
2. Check if MovementSettings allows this posture from current mode
3. Return clearance OK and setting allows
#### `SetSprinting` → `void`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `bSprinting` | `Boolean` | Whether to sprint |
- **Flow:**
1. If bSprinting and CanSprint(): SetMovementMode(Sprinting)
2. If !bSprinting: restore previous movement mode
3. Fire OnSprintStateChanged
#### `SetCrouching` → `void`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `bCrouching` | `Boolean` | Whether to crouch |
- **Flow:**
1. If bCrouching: SetPosture(Crouching), SetMovementMode(CrouchWalk)
2. If !bCrouching: SetPosture(Standing), restore previous walk mode
#### `ApplyMovementPenalty` → `void`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `Multiplier` | `Float` | Speed multiplier [0.0 - 1.0] |
| `Duration` | `Float` | How long the penalty lasts |
| `PenaltyTag` | `GameplayTag` | Identifier (e.g. Injury, Fear, SlowEffect) |
- **Flow:**
1. Modify current MovementSettings[CurrentMovementMode].MaxWalkSpeed *= Multiplier
2. Start timer for Duration
3. On timer end: restore original MaxWalkSpeed for this mode
### Protected / Private Functions
#### `OnMovementUpdated (Tick)` → `void`
- **Description:** Called each tick. Updates speed tracking, detects movement start/stop, checks ground state.
- **Flow:**
1. Get velocity from CharacterMovementComponent
2. CurrentSpeed = velocity.Size()
3. OldMoving = bIsMoving
4. bIsMoving = CurrentSpeed > 10.0
5. If bIsMoving != OldMoving: fire OnMovementStart or OnMovementStop
6. Check bIsOnGround from movement component
#### `CalculateFootstep` → `S_FootstepProfile`
- **Description:** Selects the correct footstep sound based on surface and movement mode.
- **Flow:**
1. Perform line trace from foot location downward
2. Get surface type from physical material
3. Look up S_FootstepProfile matching SurfaceType and CurrentMovementMode
4. If no match: return DefaultFootstepProfile
---
## 5. Event Dispatchers
| Dispatcher | Parameters | Bind Access | Description |
|------------|-----------|-------------|-------------|
| `OnMovementModeChanged` | `E_MovementMode OldMode`, `E_MovementMode NewMode` | `Public` | Fired when movement mode changes |
| `OnPostureChanged` | `E_PostureState OldPosture`, `E_PostureState NewPosture` | `Public` | Fired when posture changes |
| `OnMovementStart` | `none` | `Public` | Fired when the player starts moving from idle |
| `OnMovementStop` | `none` | `Public` | Fired when the player stops moving |
| `OnSprintStateChanged` | `bool bIsSprinting` | `Public` | Fired when sprint state toggles |
| `OnJumped` | `none` | `Public` | Fired when the player jumps |
| `OnLanded` | `float FallVelocity` | `Public` | Fired when the player lands after a fall |
| `OnClimbStarted` | `AActor ClimbableActor` | `Public` | Fired when climbing begins |
| `OnClimbEnded` | `none` | `Public` | Fired when climbing ends |
| `OnFootstep` | `E_MovementMode Mode`, `EPhysicalSurface Surface` | `Public` | Fired on each footstep for audio |
---
## 6. Overridden Events / Custom Events
### Event: `BeginPlay`
- **Description:** Initialises state, caches references, binds to owner's character movement events.
- **Flow:**
1. Get Owner as Character
2. Cache CharacterMovementComponent and CapsuleComponent
3. Bind to Character's OnJumped and OnLanded delegates
4. Set initial MovementMode = Idle, Posture = Standing
5. Apply default MovementSettings to movement component
### Event: `Tick (if enabled)`
- **Description:** Updates speed tracking and detects transitions.
- **Flow:**
1. Call OnMovementUpdated
2. Check for automatic posture transitions (e.g., falling -> standing on land)
### Custom Event: `OnTakeDamageAffectsMovement`
- **Description:** Listener bound to BPC_HealthSystem.OnDamageTaken.
- **Flow:**
1. If damage was significant (> 20% health): apply brief movement penalty
2. Penalty severity scales with damage amount
---
## 7. Blueprint Graph Logic Flow
```mermaid
flowchart TD
A[SetMovementMode called] --> B{Valid transition?}
B -->|No| C[Return]
B -->|Yes| D[Store PreviousMode]
D --> E[Set CurrentMode = NewMode]
E --> F[Apply MovementSettings to CharMoveComp]
F --> G[Fire OnMovementModeChanged]
G --> H{NewMode == Sprinting?}
H -->|Yes| I[Notify BPC_StaminaSystem: StartContinuousDrain]
H -->|No| J{PreviousMode == Sprinting?}
J -->|Yes| K[Notify BPC_StaminaSystem: StopContinuousDrain]
J -->|No| L[Update ABP_GASP blend values]
I --> L
K --> L
```
---
## 8. Communication Matrix
| Who Talks | How | What Is Sent |
|-----------|-----|-------------|
| `BPC_MovementStateSystem` | `Dispatcher` | `OnMovementModeChanged` -> `ABP_GASP`, `BPC_StaminaSystem`, `BPC_CameraStateLayer` |
| `BPC_MovementStateSystem` | `Dispatcher` | `OnPostureChanged` -> `ABP_GASP`, `BPC_InteractionDetector`, `BP_AudioManager` |
| `BPC_MovementStateSystem` | `Dispatcher` | `OnFootstep` -> `BP_AudioManager` (footstep SFX) |
| `BPC_MovementStateSystem` | `Dispatcher` | `OnJumped` / `OnLanded` -> `BPC_StaminaSystem` (landing drain) |
| `BPC_MovementStateSystem` | `Dispatcher` | `OnSprintStateChanged` -> `BPC_StaminaSystem`, `BPC_CameraStateLayer` (FOV) |
| `External (Input)` | `Direct` | `PC_PlayerController` calls SetMovementMode / SetPosture |
| `BPC_MovementStateSystem` | `Listener` | Binds to `BPC_HealthSystem.OnDamageTaken` for injury penalties |
---
## 9. Validation / Testing Checklist
- [ ] Movement mode changes correctly apply speed/accel values to CharacterMovementComponent
- [ ] Posture changes correctly adjust capsule height and collision
- [ ] Prone requires crouch intermediate transition
- [ ] Sprinting auto-stops when stamina hits 0 (via stamina listener)
- [ ] Movement penalties correctly modify MaxWalkSpeed and restore after duration
- [ ] OnMovementStart/OnMovementStop fire correctly at speed thresholds
- [ ] Footstep profile selection works for all surface types
- [ ] Edge case: Forced transition (knockback) applies impulse regardless of state
- [ ] Edge case: CanTransitionToPosture returns false when ceiling is too low
- [ ] Edge case: Rapid mode switching does not cause animation glitches (blend time enforced)
---
## 10. Reuse Notes
- This component is the central "movement oracle" — other systems query posture/mode instead of polling the CharacterMovementComponent.
- Footstep profiles should be defined in a Data Asset (DA_FootstepProfileTable) for easy content team iteration.
- The component does NOT handle input — that remains in PC_PlayerController.
- For AI characters, a simplified version can expose only Walking and Sprinting modes with no posture system.
- GASP integration: set GASP-specific variables (bStrafing, bSprinting) in the AnimationBlueprint on each mode change.
---
*Blueprint Spec: Movement State System. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.*

View File

@@ -0,0 +1,328 @@
# 12 — Hiding System (`BPC_HidingSystem`)
## Purpose
Manages the player's ability to hide inside, behind, or under environmental objects. Handles entering/exiting hiding spots, line-of-sight checks against enemies, peeking, and stress reduction while concealed. The central component for stealth gameplay.
## Dependencies
- **Requires:** `FL_GameUtilities` (interface casts), `I_HidingSpot` (interface on world actors)
- **Required By:** `AI_EnemyController` (query hiding state for awareness), `BPC_StressSystem` (stress decay while hidden), `BPC_PlayerController` (movement/input restriction)
- **Engine/Plugin Requirements:** GameplayTags, LineTraceByChannel (LOS checks)
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `ActorComponent` |
| **Class Type** | Blueprint Component |
| **Asset Path** | `Content/Framework/Player/BPC_HidingSystem` |
| **Implements Interfaces** | None |
---
## 1. Enums
### `E_HideState`
| Value | Description |
|-------|-------------|
| `Exposed = 0` | Not hiding, fully visible |
| `Entering = 1` | Transient — animation playing to enter hide spot |
| `Hidden = 2` | Inside hide spot, concealed |
| `Peeking = 3` | Partially visible, can look around |
| `Exiting = 4` | Transient — animation playing to exit hide spot |
### `E_HideType`
| Value | Description |
|-------|-------------|
| `Locker = 0` | Fully enclosed (locker, wardrobe) |
| `BehindCover = 1` | Behind low wall, crate, counter |
| `Under = 2` | Under bed, table, porch |
| `InShadow = 3` | Standing in a shadow volume |
| `TallGrass = 4` | Crouch-moving through vegetation |
### `E_PeekDirection`
| Value | Description |
|-------|-------------|
| `Left = 0` | Peek left from behind cover |
| `Right = 1` | Peek right from behind cover |
| `Over = 2` | Peek over low cover |
---
## 2. Structs
### `S_HideSpotInfo`
| Field | Type | Description |
|-------|------|-------------|
| `HidingActor` | `AActor` | The hide spot actor |
| `HideType` | `E_HideType` | Type of hiding |
| `SlotCount` | `Integer` | How many can hide here (-1 = infinite) |
| `bHasPeekAbility` | `Boolean` | Can player peek from this spot? |
| `PeekSocketLeft` | `FName` | Socket name for left peek camera |
| `PeekSocketRight` | `FName` | Socket name for right peek camera |
| `PeekSocketOver` | `FName` | Socket name for over peek camera |
| `ExitLocation` | `Vector` | World location to place player on exit |
| `ExitRotation` | `Rotator` | Rotation to face on exit |
| `bBlocksStress` | `Boolean` | Whether this spot reduces stress gain |
| `HideTags` | `GameplayTagContainer` | Tags for filtering |
| `LOSTraceChannel` | `TEnumAsByte<ETraceTypeQuery>` | Trace channel for LOS checks from this spot |
---
## 3. Variables
### Configuration (Instance Editable, Expose On Spawn)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `bCanHide` | `Boolean` | `true` | `Hiding Config` | Can this player hide at all? |
| `StressDecayWhileHidden` | `Float` | `5.0` | `Hiding Config` | Stress lost per second while hidden |
| `bEnemyCanFindHidingSpot` | `Boolean` | `true` | `Hiding Config` | Can enemies discover this spot |
| `MaxPeekDuration` | `Float` | `3.0` | `Hiding Config` | Max seconds player can peek before forced back |
| `PeekCooldown` | `Float` | `1.5` | `Hiding Config` | Seconds before player can peek again |
| `CapsuleCheckRadius` | `Float` | `50.0` | `Hiding Config` | Radius for capsule trace when finding hide spots |
### Internal (Private / Protected, No Expose)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `CurrentHideState` | `E_HideState` | `Exposed` | `Hiding State` | Current hiding state |
| `PreviousHideState` | `E_HideState` | `Exposed` | `Hiding State` | Previous state for transition detection |
| `CurrentHideSpot` | `S_HideSpotInfo` | `-` | `Hiding State` | Active hide spot data |
| `PendingHideSpot` | `AActor` | `None` | `Hiding State` | Hide spot being entered |
| `PeekTimerHandle` | `FTimerHandle` | `-` | `Hiding State` | Timer for max peek duration |
| `PeekCooldownTimerHandle` | `FTimerHandle` | `-` | `Hiding State` | Timer for peek cooldown |
| `bCanPeek` | `Boolean` | `true` | `Hiding State` | Whether peeking is allowed |
| `LastLOSCheckTime` | `Float` | `0.0` | `Hiding State` | World time of last LOS check |
| `LOSTimerHandle` | `FTimerHandle` | `-` | `Hiding State` | Timer for periodic LOS checks |
| `BreathHeldTimer` | `FTimerHandle` | `-` | `Hiding State` | Timer for breath-hold mechanic |
| `bIsHoldingBreath` | `Boolean` | `false` | `Hiding State` | Whether player is holding breath |
### Replicated (if multiplayer)
| Variable | Type | Condition | Description |
|----------|------|-----------|-------------|
| `CurrentHideState` | `E_HideState` | `RepNotify` | Replicated hide state |
| `CurrentHideSpot` | `S_HideSpotInfo` | `Replicated` | Replicated spot reference |
---
## 4. Functions
### Public Functions
#### `EnterHideSpot` → `Boolean`
- **Description:** Attempts to enter a hiding spot. Returns true if successful.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `HideSpotActor` | `AActor` | The hide spot to enter |
- **Blueprint Authority:** Server (if MP), Any (single-player)
- **Flow:**
1. Early-out if CurrentHideState != Exposed or !bCanHide
2. Validate that HideSpotActor implements I_HidingSpot
3. Check slot availability (I_HidingSpot.Execute_HasAvailableSlots)
4. If available:
- Set CurrentHideState = Entering
- Store HideSpotActor info in CurrentHideSpot
- Disable player movement input
- Play entering animation (via ABP_GASP)
- On animation complete: Set CurrentHideState = Hidden
- Fire OnHideStateChanged
- Start stress decay timer
- Start periodic LOS check timer
- Return true
5. If full: return false
#### `ExitHideSpot` → `void`
- **Description:** Exits the current hiding spot.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `bForceExit` | `Boolean` | If true, skip exit animation |
- **Flow:**
1. If CurrentHideState != Hidden and CurrentHideState != Peeking: return
2. Set CurrentHideState = Exiting
3. If bForceExit: skip animation, immediately set Exposed
4. Else: play exiting animation
5. On animation complete or bForceExit:
- Teleport player to CurrentHideSpot.ExitLocation with ExitRotation
- Re-enable movement input
- Set CurrentHideState = Exposed
- Clear CurrentHideSpot
- Stop stress decay and LOS timers
- Fire OnHideStateChanged
- Fire OnExitedHidingSpot
#### `StartPeek` → `void`
- **Description:** Begins peeking from behind cover.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `Direction` | `E_PeekDirection` | Which direction to peek |
- **Flow:**
1. Early-out if CurrentHideState != Hidden or !bCanPeek or !CurrentHideSpot.bHasPeekAbility
2. Set CurrentHideState = Peeking
3. Move camera to peek socket location
4. Start MaxPeekDuration timer
5. Start PeekCooldown timer (bCanPeek = false during cooldown)
6. Fire OnPeekStarted
#### `StopPeek` → `void`
- **Description:** Ends the peek and returns to Hidden state.
- **Flow:**
1. If CurrentHideState != Peeking: return
2. Set CurrentHideState = Hidden
3. Return camera to default hide position
4. Fire OnPeekEnded
#### `IsPlayerDetectable` → `Boolean`
- **Description:** Checks if the player is detectable from a given enemy location, considering hide type and edges.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `EnemyLocation` | `Vector` | Location of the enemy |
| `DetectionRange` | `Float` | Max range for detection check |
- **Flow:**
1. If CurrentHideState != Hidden: return true (fully detectable)
2. If EnemyLocation distance > DetectionRange: return false
3. Perform line trace from enemy to player's approximate position
4. If trace hits CurrentHideSpot.HidingActor before player: return false (blocked by cover)
5. If trace hits player directly: return true (cover not fully effective)
6. Edge case: peek state increases detection radius
#### `TryBreathHold` → `void`
- **Description:** Player holds breath to reduce noise/detectability for a short time.
- **Flow:**
1. bIsHoldingBreath = true
2. Start breath-hold timer (e.g., 8 seconds)
3. On timer end: forced exhale, brief noise event, bIsHoldingBreath = false
4. Fire OnBreathHoldChanged
### Protected / Private Functions
#### `PerformLOSCheck` → `void`
- **Description:** Timer callback — checks if any enemy has line of sight to this hide spot.
- **Flow:**
1. Get all AI_EnemyController instances within detection range
2. For each enemy: call IsPlayerDetectable(EnemyLocation)
3. If any enemy has LOS:
- Fire OnHideSpotCompromised
- If CurrentHideSpot.bEnemyCanFindHidingSpot: fire OnForcedExitWarning
#### `OnAnimationHideEnterComplete` → `void`
- **Description:** Animation notify callback when enter animation finishes.
- **Flow:**
1. Set CurrentHideState = Hidden
2. Attach player to hide spot socket if applicable
3. Fire OnHideStateChanged
4. Start stress decay loop
#### `OnAnimationHideExitComplete` → `void`
- **Description:** Animation notify callback when exit animation finishes.
- **Flow:**
1. Perform final exit steps already queued in ExitHideSpot
---
## 5. Event Dispatchers
| Dispatcher | Parameters | Bind Access | Description |
|------------|-----------|-------------|-------------|
| `OnHideStateChanged` | `E_HideState OldState`, `E_HideState NewState` | `Public` | Fired on any hide state change |
| `OnEnteredHidingSpot` | `AActor HideSpot`, `E_HideType HideType` | `Public` | Fired when player successfully hides |
| `OnExitedHidingSpot` | `AActor HideSpot` | `Public` | Fired when player leaves hiding |
| `OnPeekStarted` | `E_PeekDirection Direction` | `Public` | Fired when player starts peeking |
| `OnPeekEnded` | `none` | `Public` | Fired when player stops peeking |
| `OnHideSpotCompromised` | `AActor EnemyActor` | `Public` | Fired when an enemy sees the player's spot |
| `OnForcedExitWarning` | `float TimeUntilDiscovered` | `Public` | Fired when enemy is approaching known spot |
| `OnBreathHoldChanged` | `bool bIsHoldingBreath` | `Public` | Fired when breath hold toggles |
---
## 6. Overridden Events / Custom Events
### Event: `BeginPlay`
- **Description:** Initialises state, binds to input actions.
- **Flow:**
1. Set CurrentHideState = Exposed
2. Register with GI_GameTagRegistry if needed
3. Cache reference to CharacterMovementComponent for disabling during hide
### Custom Event: `ForceKickFromHide`
- **Description:** Called externally (e.g., by enemy discovery) to force player out of hiding.
- **Flow:**
1. Call ExitHideSpot(bForceExit = true)
2. Apply brief stun or camera shake
3. Fire OnForcedExitWarning with 0 seconds
### Custom Event: `OnDamageWhileHiding`
- **Description:** Listener for when damage is taken while hidden.
- **Flow:**
1. If damage source penetrates cover: force exit
2. Else: reduce stress instead of health (environmental damage)
---
## 7. Blueprint Graph Logic Flow
```mermaid
flowchart TD
A[EnterHideSpot called] --> B{Is Exposed?}
B -->|No| C[Return false]
B -->|Yes| D[Validate I_HidingSpot]
D --> E{Has available slots?}
E -->|No| F[Return false]
E -->|Yes| G[Set Entering state]
G --> H[Disable movement input]
H --> I[Play Enter animation]
I --> J[Animation notify: OnComplete]
J --> K[Set Hidden state]
K --> L[Start stress decay loop]
L --> M[Start periodic LOS check]
M --> N[Fire OnEnteredHidingSpot]
N --> O[Return true]
```
---
## 8. Communication Matrix
| Who Talks | How | What Is Sent |
|-----------|-----|-------------|
| `BPC_HidingSystem` | `Dispatcher` | `OnHideStateChanged` -> `BPC_PlayerController` (input), `ABP_GASP` (anim), `BPC_StressSystem` (decay), `AI_EnemyController` (awareness reset) |
| `BPC_HidingSystem` | `Dispatcher` | `OnHideSpotCompromised` -> `BPC_StressSystem` (stress spike), `BP_AudioManager` (tension music) |
| `BPC_HidingSystem` | `Dispatcher` | `OnForcedExitWarning` -> `WBP_HUD` (hide icon flash), `BP_AudioManager` (heartbeat) |
| `External (AI)` | `Query` | `IsPlayerDetectable` called by `AI_EnemyController` for LOS checks |
| `External (World)` | `Interface` | `I_HidingSpot` on `BP_Locker`, `BP_Wardrobe`, `BP_Cover` |
| `BPC_HidingSystem` | `Dispatcher` | `OnPeekStarted/Ended` -> `BPC_CameraStateLayer` (FOV/position) |
---
## 9. Validation / Testing Checklist
- [ ] EnterHideSpot correctly transitions through Entering -> Hidden
- [ ] ExitHideSpot correctly transitions through Exiting -> Exposed
- [ ] Forced exit with bForceExit = true skips animation, immediately exits
- [ ] Peek timer forces player back to Hidden after MaxPeekDuration
- [ ] PeekCooldown prevents rapid peek toggling
- [ ] IsPlayerDetectable returns correct values for all hide types and peek states
- [ ] Multiple hide spots with limited slots: full slots reject entry
- [ ] Stress decays while hidden at configured rate
- [ ] LOS check detects enemies within range and fires compromise dispatcher
- [ ] Edge case: EnterHideSpot called while already hiding returns false
- [ ] Edge case: ExitHideSpot called while Exposed does nothing
- [ ] Edge case: Destroying hide spot while inside force-exits player
---
## 10. Reuse Notes
- Enemy AI characters can use a simplified version to find and occupy hide spots.
- Hide spots can be pre-placed (lockers, beds) or dynamic (shadow volumes, tall grass).
- The LOS trace channel should be custom (e.g., DetectionChannel) to ignore small props.
- Breath-hold mechanic adds depth to stealth gameplay near enemies.
- For multiplayer: only the hiding player knows their state; other players see a generic "occupado" on the spot.
- Hide spots with bBlocksStress = true can double as narrative safe rooms.
---
*Blueprint Spec: Hiding System. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.*

View File

@@ -0,0 +1,258 @@
# 13 — Embodiment System (`BPC_EmbodimentSystem`)
## Purpose
Creates a sense of first-person body awareness — the player can see their own body, shadow, arms, and legs in appropriate contexts. Manages body visibility, shadow casting, limb IK, and environmental body interactions (body brushing against walls, water drip on arms, blood spatter on hands).
## Dependencies
- **Requires:** `BPC_MovementStateSystem` (posture affects body visibility), `BPC_HealthSystem` (blood spatter based on damage)
- **Required By:** `BPC_CameraStateLayer` (head height offsets), `BP_PlayerCharacter` (component composition)
- **Engine/Plugin Requirements:** GASP (arms animation), Material Parameter Collections (blood/water overlays)
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `ActorComponent` |
| **Class Type** | Blueprint Component |
| **Asset Path** | `Content/Framework/Player/BPC_EmbodimentSystem` |
| **Implements Interfaces** | None |
---
## 1. Enums
### `E_BodyVisibilityMode`
| Value | Description |
|-------|-------------|
| `FullBody = 0` | Full body visible (third person, mirrors, death) |
| `ArmsOnly = 1` | Only arms/hands visible (default first person) |
| `ArmsAndShadow = 2` | Arms + body shadow visible on ground |
| `Minimal = 3` | Only hands visible (high stress, underwater) |
| `Hidden = 4` | Body fully hidden (cutscene, UI menu) |
### `E_BodyOverlayState`
| Value | Description |
|-------|-------------|
| `Clean = 0` | No overlays |
| `BloodSpatter = 1` | Blood on hands/arms |
| `WaterDroplets = 2` | Water on arms/camera |
| `Mud = 3` | Mud/dirt on body |
| `Toxic = 4` | Green/glowing contamination |
---
## 2. Structs
### `S_BodyOverlay`
| Field | Type | Description |
|-------|------|-------------|
| `OverlayType` | `E_BodyOverlayState` | Which overlay to apply |
| `MaterialParameter` | `FName` | Parameter name on the material |
| `TargetValue` | `Float` | Target intensity [0..1] |
| `BlendSpeed` | `Float` | How fast to blend to target |
| `Duration` | `Float` | How long the overlay lasts before fading |
### `S_BodyPartVisibility`
| Field | Type | Description |
|-------|------|-------------|
| `bShowArms` | `Boolean` | Whether arms are visible |
| `bShowLegs` | `Boolean` | Whether legs are visible |
| `bShowShadow` | `Boolean` | Whether body shadow is visible |
| `bShowTorso` | `Boolean` | Whether torso is visible (mirrors) |
---
## 3. Variables
### Configuration (Instance Editable, Expose On Spawn)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `DefaultVisibility` | `E_BodyVisibilityMode` | `ArmsOnly` | `Embodiment Config` | Default visibility for normal gameplay |
| `ArmsMesh` | `SkeletalMeshComponent` | `None` | `Embodiment Config` | First-person arms mesh reference |
| `BodyShadowComponent` | `UShadowComponent` | `None` | `Embodiment Config` | Shadow-only body component |
| `bEnableBodyBrushing` | `Boolean` | `true` | `Embodiment Config` | Arms react to nearby walls |
| `BrushTraceDistance` | `Float` | `30.0` | `Embodiment Config` | Distance to trace for wall detection |
| `OverlayBlendGlobalSpeed` | `Float` | `2.0` | `Embodiment Config` | Default blend speed for all overlays |
### Internal (Private / Protected, No Expose)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `CurrentVisibility` | `E_BodyVisibilityMode` | `ArmsOnly` | `Embodiment State` | Current visibility mode |
| `CurrentOverlay` | `E_BodyOverlayState` | `Clean` | `Embodiment State` | Current active overlay |
| `OverlayIntensity` | `Float` | `0.0` | `Embodiment State` | Current overlay intensity [0..1] |
| `bIsNearWall` | `Boolean` | `false` | `Embodiment State` | Whether wall is near for arm brushing |
| `LastOverLayTime` | `Float` | `0.0` | `Embodiment State` | When last overlay was applied |
| `ActiveOverlays` | `Map<E_BodyOverlayState, S_BodyOverlay>` | `{}` | `Embodiment State` | Currently active overlays with blend info |
| `OverlayTimerHandle` | `FTimerHandle` | `-` | `Embodiment State` | Timer for overlay fade-out |
### Replicated (if multiplayer)
| Variable | Type | Condition | Description |
|----------|------|-----------|-------------|
| `CurrentVisibility` | `E_BodyVisibilityMode` | `Replicated` | Synced visibility mode |
| `CurrentOverlay` | `E_BodyOverlayState` | `Replicated` | Synced overlay state |
---
## 4. Functions
### Public Functions
#### `SetVisibilityMode` → `void`
- **Description:** Sets body visibility mode and toggles mesh components.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `NewMode` | `E_BodyVisibilityMode` | Desired visibility mode |
| `bInstant` | `Boolean` | If true, skip fade transition |
- **Flow:**
1. CurrentVisibility = NewMode
2. Apply S_BodyPartVisibility based on NewMode
3. Set ArmsMesh visibility based on bShowArms
4. Set BodyShadowComponent visibility based on bShowShadow
5. Fire OnVisibilityModeChanged
#### `ApplyOverlay` → `void`
- **Description:** Applies a body overlay (blood, water, mud) with blend.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `Overlay` | `S_BodyOverlay` | Overlay to apply |
- **Flow:**
1. Add or update ActiveOverlays[Overlay.OverlayType]
2. Start blend toward TargetValue at Overlay.BlendSpeed
3. If Overlay.Duration > 0: start timer to fade out after Duration
4. Update material parameter on ArmsMesh
5. Fire OnOverlayChanged
#### `ClearOverlay` → `void`
- **Description:** Clears a specific overlay or all overlays.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `OverlayType` | `E_BodyOverlayState` | Type to clear; None = clear all |
- **Flow:**
1. If OverlayType specified: remove single overlay, blend intensity to 0
2. If OverlayType == None: clear all overlays, reset to Clean
3. Update material parameters
4. Fire OnOverlayChanged
#### `CheckWallProximity` → `void`
- **Description:** Traces forward from camera to detect nearby walls for arm brushing.
- **Flow:**
1. Perform line trace from camera forward by BrushTraceDistance
2. If hit: bIsNearWall = true, apply arm IK offset
3. If no hit: bIsNearWall = false, relax arm IK
4. Fire OnWallProximityChanged
#### `GetBodyPartVisibility` → `S_BodyPartVisibility`
- **Description:** Returns current visibility state for all body parts.
- **Flow:** Return struct derived from CurrentVisibility
### Protected / Private Functions
#### `ApplyOverlayBlendTick` → `void`
- **Description:** Called each tick — blends all active overlay material parameters.
- **Flow:**
1. For each ActiveOverlay: blend material parameter toward TargetValue
2. If blend complete and Duration expired: begin fade to 0
3. Set material parameters on ArmsMesh
---
## 5. Event Dispatchers
| Dispatcher | Parameters | Bind Access | Description |
|------------|-----------|-------------|-------------|
| `OnVisibilityModeChanged` | `E_BodyVisibilityMode OldMode`, `E_BodyVisibilityMode NewMode` | `Public` | Fired when body visibility changes |
| `OnOverlayChanged` | `E_BodyOverlayState CurrentOverlay`, `float Intensity` | `Public` | Fired when overlay state changes |
| `OnWallProximityChanged` | `bool bIsNearWall`, `float Distance` | `Public` | Fired when wall proximity changes |
| `OnBodyPartHit` | `FName BoneName`, `S_DamageEvent DamageEvent` | `Public` | Fired when a visible body part is hit |
---
## 6. Overridden Events / Custom Events
### Event: `BeginPlay`
- **Description:** Caches mesh references, sets initial visibility.
- **Flow:**
1. Find ArmsMesh on owner (first-person skeletal mesh)
2. Find BodyShadowComponent
3. Set initial visibility to DefaultVisibility
4. Bind to BPC_HealthSystem.OnDamageTaken for blood overlay
5. Start wall proximity tick if bEnableBodyBrushing
### Custom Event: `OnDamageTakenBloodHandler`
- **Description:** Applies blood spatter overlay when player takes damage.
- **Flow:**
1. Intensity = FMath::Min(DamageEvent.BaseAmount / 50.0, 1.0)
2. Build S_BodyOverlay with BloodSpatter type
3. Call ApplyOverlay
### Custom Event: `OnEnterWaterHandler`
- **Description:** Fired when player enters water volume.
- **Flow:**
1. Apply WaterDroplets overlay with fast blend and short duration
2. Start drying timer for overlay fade
---
## 7. Blueprint Graph Logic Flow
```mermaid
flowchart TD
A[SetVisibilityMode called] --> B{NewMode == Current?}
B -->|Yes| C[Return]
B -->|No| D[Determine BodyPartVisibility]
D --> E{NewMode == ArmsOnly?}
E -->|Yes| F[Enable Arms, disable shadow/torso]
E -->|No| G{NewMode == FullBody?}
G -->|Yes| H[Enable all body parts]
G -->|No| I{NewMode == Hidden?}
I -->|Yes| J[Disable all meshes]
I -->|No| K[Apply partial visibility]
F --> L[Fire OnVisibilityModeChanged]
H --> L
J --> L
K --> L
```
---
## 8. Communication Matrix
| Who Talks | How | What Is Sent |
|-----------|-----|-------------|
| `BPC_EmbodimentSystem` | `Dispatcher` | `OnVisibilityModeChanged` -> `BPC_CameraStateLayer` (FOV adjust) |
| `BPC_EmbodimentSystem` | `Dispatcher` | `OnOverlayChanged` -> `WBP_HUD` (screen effects), `BP_AudioManager` |
| `BPC_EmbodimentSystem` | `Dispatcher` | `OnWallProximityChanged` -> `ABP_Arms` (IK offset) |
| `BPC_HealthSystem` | `Listener` | `OnDamageTaken` triggers blood overlay |
| `External (BP_WaterVolume)` | `Direct` | Calls `ApplyOverlay` for water effects |
---
## 9. Validation / Testing Checklist
- [ ] Visibility modes correctly toggle mesh component visibility
- [ ] Blood overlay appears on damage and fades over time
- [ ] Water overlay appears on water entry and dries gradually
- [ ] Wall proximity trace correctly detects nearby walls
- [ ] Arm IK offsets apply when wall is detected
- [ ] Edge case: Rapid visibility mode changes do not cause rendering glitches
- [ ] Edge case: Multiple overlays stack correctly (blood + water = combined)
- [ ] Edge case: Hidden mode hides all body components regardless of other settings
---
## 10. Reuse Notes
- The overlay system uses Material Parameter Collections — the implementation should expose scalar parameters for blood/water/mud blend amounts.
- Wall proximity uses a simple line trace; more advanced IK requires the arms animation blueprint.
- For third-person modes, use a separate full-body skeletal mesh instead of the arms mesh.
- Overlay intensity can be driven by gameplay values: more damage = more blood, longer swim = more water.
- The body shadow component is a separate capsule with a shadow-only material — not a full character mesh.
---
*Blueprint Spec: Embodiment System. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.*

View File

@@ -0,0 +1,329 @@
# 14 — Camera State Layer (`BPC_CameraStateLayer`)
## Purpose
Manages the first-person camera's dynamic behaviour: field-of-view changes based on movement speed, camera shake from impacts/events, head bob intensity, and location offsets for crouch/prone/hide. Centralises all camera-modifying logic so other systems can request camera changes without directly manipulating the camera.
## Dependencies
- **Requires:** `BPC_MovementStateSystem` (speed-based FOV), `BPC_StressSystem` (stress-based distortion)
- **Required By:** `PC_PlayerController` (camera management), `BPC_HidingSystem` (peek camera offsets), `WBP_HUD` (post-process materials)
- **Engine/Plugin Requirements:** Camera Shake patterns, Post-Process Materials, Material Parameter Collections
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `ActorComponent` |
| **Class Type** | Blueprint Component |
| **Asset Path** | `Content/Framework/Player/BPC_CameraStateLayer` |
| **Implements Interfaces** | None |
---
## 1. Enums
### `E_CameraState`
| Value | Description |
|-------|-------------|
| `Default = 0` | Normal gameplay FOV |
| `Aiming = 1` | Zoomed in (weapon aim, inspection) |
| `Sprinting = 2` | FOV widened for speed |
| `Crouching = 3` | Slight downward offset |
| `Peeking = 4` | Offset sideways from cover |
| `Hiding = 5` | Constricted FOV, darkness from inside |
| `Stressed = 6` | FOV pulsing, chromatic aberration |
| `Injured = 7` | FOV wobble, blood tint |
| `Death = 8` | Fade to black, camera drop |
| `Cutscene = 9` | Fixed camera, no player control |
### `E_CameraShakePriority`
| Value | Description |
|-------|-------------|
| `Low = 0` | Ambient shake (footstep, wind) |
| `Medium = 1` | Impact shake (damage, explosion) |
| `High = 2` | Critical shake (near-death, supernatural event) |
| `Cinematic = 3` | Scripted shake, overrides everything |
---
## 2. Structs
### `S_CameraShakeRequest`
| Field | Type | Description |
|-------|------|-------------|
| `ShakeClass` | `TSubclassOf<UCameraShakeBase>` | Camera shake blueprint class |
| `Scale` | `Float` | Intensity scale [0..1] |
| `Priority` | `E_CameraShakePriority` | Priority level |
| `Duration` | `Float` | How long the shake lasts |
| `bIsLooping` | `Boolean` | Whether the shake loops |
| `Tag` | `GameplayTag` | Unique identifier to prevent stacking same shake |
### `S_CameraStateConfig`
| Field | Type | Description |
|-------|------|-------------|
| `TargetFOV` | `Float` | FOV value for this state |
| `BlendSpeed` | `Float` | How fast to blend to this FOV |
| `HeadOffset` | `Vector` | Location offset for head/camera |
| `HeadRotation` | `Rotator` | Rotation offset |
| `bConstrainPitch` | `Boolean` | Limit vertical look in this state |
| `MinPitch` | `Float` | Minimum pitch angle |
| `MaxPitch` | `Float` | Maximum pitch angle |
| `bEnableHeadBob` | `Boolean` | Enable head bob in this state |
| `HeadBobAmplitude` | `Float` | Head bob strength |
| `HeadBobFrequency` | `Float` | Head bob speed |
### `S_PostProcessOverride`
| Field | Type | Description |
|-------|------|-------------|
| `bOverrideVignette` | `Boolean` | Override vignette intensity |
| `VignetteIntensity` | `Float` | Vignette strength [0..1] |
| `bOverrideChromaticAberration` | `Boolean` | Override CA intensity |
| `ChromaticAberrationIntensity` | `Float` | CA strength [0..1] |
| `bOverrideColorGrading` | `Boolean` | Override color grading |
| `ColorGradingLUT` | `Texture` | LUT texture for color grading |
| `BlendTime` | `Float` | Time to blend to new post-process settings |
---
## 3. Variables
### Configuration (Instance Editable, Expose On Spawn)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `DefaultFOV` | `Float` | `90.0` | `Camera Config` | Default first-person FOV |
| `StateConfigs` | `Map<E_CameraState, S_CameraStateConfig>` | `all states` | `Camera Config` | FOV/offset values per state |
| `DefaultPostProcess` | `S_PostProcessOverride` | `-` | `Camera Config` | Default post-process settings |
| `BaseHeadBobAmplitude` | `Float` | `0.1` | `Camera Config` | Default head bob strength |
| `BaseHeadBobFrequency` | `Float` | `1.0` | `Camera Config` | Default head bob speed |
| `SprintFOVMultiplier` | `Float` | `1.1` | `Camera Config` | FOV multiplied by this when sprinting |
| `StressFOVPulseAmount` | `Float` | `5.0` | `Camera Config` | FOV pulse range when stressed (degrees) |
| `StressFOVPulseSpeed` | `Float` | `0.5` | `Camera Config` | Speed of stress FOV pulse |
### Internal (Private / Protected, No Expose)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `CurrentCameraState` | `E_CameraState` | `Default` | `Camera State` | Active camera state |
| `CurrentTargetFOV` | `Float` | `90.0` | `Camera State` | Current FOV target being blended to |
| `CurrentFOV` | `Float` | `90.0` | `Camera State` | Actual current FOV |
| `ActiveShakeRequests` | `Map<FName, S_CameraShakeRequest>` | `{}` | `Camera State` | Active shakes by tag |
| `HighestShakePriority` | `E_CameraShakePriority` | `Low` | `Camera State` | Current highest priority shake |
| `CameraOffsetTarget` | `Vector` | `(0,0,0)` | `Camera State` | Target head offset being blended to |
| `CameraRotationTarget` | `Rotator` | `(0,0,0)` | `Camera State` | Target rotation offset |
| `bIsPitchConstrained` | `Boolean` | `false` | `Camera State` | Whether pitch is currently constrained |
| `PostProcessBlendAlpha` | `Float` | `0.0` | `Camera State` | Current post-process blend |
| `FOVTimerHandle` | `FTimerHandle` | `-` | `Camera State` | Timer for FOV pulse when stressed |
| `PlayerCameraManager` | `APlayerCameraManager` | `None` | `Camera State` | Cached camera manager reference |
### Replicated (if multiplayer)
| Variable | Type | Condition | Description |
|----------|------|-----------|-------------|
| `CurrentCameraState` | `E_CameraState` | `Replicated` | Synced camera state |
---
## 4. Functions
### Public Functions
#### `RequestCameraState` → `void`
- **Description:** Sets the camera to a state config. Multiple states can stack; highest priority wins.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `NewState` | `E_CameraState` | Desired camera state |
| `bImmediate` | `Boolean` | Skip blend |
- **Blueprint Authority:** Any
- **Flow:**
1. Store CurrentCameraState as PreviousState
2. CurrentCameraState = NewState
3. Get S_CameraStateConfig for NewState
4. Set CurrentTargetFOV to config.TargetFOV
5. Set CameraOffsetTarget to config.HeadOffset
6. Set CameraRotationTarget to config.HeadRotation
7. Set pitch constraints from config
8. Begin blending toward targets over BlendSpeed
9. Fire OnCameraStateChanged
#### `PlayCameraShake` → `void`
- **Description:** Plays a camera shake, respecting priority to prevent shake stacking.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `ShakeRequest` | `S_CameraShakeRequest` | Shake parameters |
- **Flow:**
1. If ShakeRequest.Tag already in ActiveShakeRequests:
- If new priority >= existing: replace existing shake
- If new priority < existing: ignore
2. Ask PlayerCameraManager to play shake
3. Store in ActiveShakeRequests
4. If Duration > 0: start timer to auto-stop shake after Duration
#### `StopCameraShake` → `void`
- **Description:** Stops a specific camera shake.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `ShakeTag` | `GameplayTag` | Tag identifying the shake |
- **Flow:**
1. If ShakeTag exists in ActiveShakeRequests:
- Stop the shake in PlayerCameraManager
- Remove from ActiveShakeRequests
#### `ApplyPostProcessOverride` → `void`
- **Description:** Applies temporary post-process settings (vignette, CA, color grading).
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `Override` | `S_PostProcessOverride` | Post-process settings |
| `bInstant` | `Boolean` | If true, blend instantly |
- **Flow:**
1. If bInstant: apply immediately
2. Else: start blend over Override.BlendTime
3. Update Material Parameter Collection values
#### `ClearPostProcessOverride` → `void`
- **Description:** Returns post-process to default settings.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `BlendTime` | `Float` | Seconds to blend back to default |
- **Flow:**
1. Blend all post-process parameters to 0 / neutral over BlendTime
#### `GetCameraStateConfig` → `S_CameraStateConfig`
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `State` | `E_CameraState` | Which state to query |
- **Flow:** Return StateConfigs[State]
### Protected / Private Functions
#### `BlendToTargetFOV (Tick)` → `void`
- **Description:** Each tick, blend CurrentFOV toward CurrentTargetFOV.
- **Flow:**
1. Delta = CurrentTargetFOV - CurrentFOV
2. If Abs(Delta) < 0.1: CurrentFOV = CurrentTargetFOV
3. Else: CurrentFOV += Delta * BlendSpeed * DeltaTime
4. Apply CurrentFOV to PlayerCameraManager
#### `BlendCameraOffset (Tick)` → `void`
- **Description:** Each tick, blend camera location/rotation toward target.
- **Flow:**
1. InterpVTo(CameraOffsetTarget) for location
2. InterpRTo(CameraRotationTarget) for rotation
3. Apply offset to camera boom or view target
#### `UpdateStressPulse` → `void`
- **Description:** If stressed, pulse FOV for uneasy effect.
- **Flow:**
1. If CurrentCameraState != Stressed: return
2. PulseValue = Sin(Time * StressFOVPulseSpeed * PI * 2) * StressFOVPulseAmount
3. CurrentTargetFOV = StateConfigs[Stressed].TargetFOV + PulseValue
---
## 5. Event Dispatchers
| Dispatcher | Parameters | Bind Access | Description |
|------------|-----------|-------------|-------------|
| `OnCameraStateChanged` | `E_CameraState OldState`, `E_CameraState NewState` | `Public` | Fired when camera state changes |
| `OnFOVChanged` | `float OldFOV`, `float NewFOV` | `Public` | Fired when FOV changes |
| `OnShakeStarted` | `GameplayTag ShakeTag`, `float Scale` | `Public` | Fired when a camera shake begins |
| `OnShakeEnded` | `GameplayTag ShakeTag` | `Public` | Fired when a camera shake ends |
| `OnPostProcessChanged` | `float Vignette`, `float ChromaticAb` | `Public` | Fired on post-process change |
| `OnCameraConstrained` | `bool bIsConstrained`, `float MinPitch`, `float MaxPitch` | `Public` | Fired when pitch constraints change |
---
## 6. Overridden Events / Custom Events
### Event: `BeginPlay`
- **Description:** Cache camera manager, set initial FOV, bind to state systems.
- **Flow:**
1. Get PlayerCameraManager from owner's controller
2. Set CurrentFOV = DefaultFOV
3. Apply to camera
4. Bind to BPC_MovementStateSystem.OnMovementModeChanged
5. Bind to BPC_StressSystem.OnStressTierChanged
### Custom Event: `OnMovementModeChangedHandler`
- **Description:** Reacts to movement changes for FOV adjustments.
- **Flow:**
1. If Sprinting: RequestCameraState(Sprinting)
2. If Crouching: RequestCameraState(Crouching)
3. If Idle/Walking: RequestCameraState(Default)
### Custom Event: `OnStressTierChangedHandler`
- **Description:** Reacts to stress tier changes for camera effects.
- **Flow:**
1. If tier >= Panicked: RequestCameraState(Stressed), apply CA post-process
2. If tier < Distressed: return to Default camera, clear post-process
---
## 7. Blueprint Graph Logic Flow
```mermaid
flowchart TD
A[RequestCameraState called] --> B{NewState == Current?}
B -->|Yes| C[Return]
B -->|No| D[Look up S_CameraStateConfig]
D --> E[Set FOV target]
E --> F[Set head offset/rotation targets]
F --> G[Set pitch constraints]
G --> H[Fire OnCameraStateChanged]
H --> I[BlendTick: each frame]
I --> J[Interp FOV toward target]
J --> K[Interp offset toward target]
K --> L[Apply to PlayerCameraManager]
L --> M{Is Stressed state?}
M -->|Yes| N[Apply FOV pulse via sine wave]
M -->|No| O[Clear pulse effect]
```
---
## 8. Communication Matrix
| Who Talks | How | What Is Sent |
|-----------|-----|-------------|
| `BPC_CameraStateLayer` | `Dispatcher` | `OnCameraStateChanged` -> `ABP_GASP` (anim offset), `WBP_HUD` (post-process) |
| `BPC_CameraStateLayer` | `Dispatcher` | `OnShakeStarted` / `OnShakeEnded` -> `BP_AudioManager` |
| `BPC_CameraStateLayer` | `Listener` | Binds to `BPC_MovementStateSystem.OnMovementModeChanged` for FOV |
| `BPC_CameraStateLayer` | `Listener` | Binds to `BPC_StressSystem.OnStressTierChanged` for distortion |
| `BPC_CameraStateLayer` | `Listener` | Binds to `BPC_HealthSystem.OnDamageTaken` for injury shake |
| `External (Any)` | `Direct` | Calls `PlayCameraShake` for any gameplay event |
| `BPC_HidingSystem` | `Direct` | Calls `RequestCameraState(Peeking)` with peek offset |
| `BPC_HidingSystem` | `Direct` | Calls `RequestCameraState(Hiding)` with hide FOV |
---
## 9. Validation / Testing Checklist
- [ ] FOV blends smoothly between states at configured speed
- [ ] Sprinting FOV wider than Default, returns on stop
- [ ] Camera shake priority prevents lower-priority shakes from overriding high-priority ones
- [ ] Stress pulsing creates smooth FOV oscillation without jitter
- [ ] Post-process overrides blend in/out correctly
- [ ] Pitch constraints apply correctly in hiding/peeking states
- [ ] Multiple state changes are queued — last request wins
- [ ] Edge case: Rapid state switching between sprint and crouch transitions smoothly
- [ ] Edge case: Camera shake with same tag replaces shake without stacking
- [ ] Edge case: ClearPostProcessOverride returns to default with correct blend
- [ ] All active shakes stop on component destroy or pawn unpossess
---
## 10. Reuse Notes
- Camera shake patterns should be authored as Blueprint Camera Shake classes in Content/Framework/Player/Shakes/.
- Post-process settings use a Material Parameter Collection (MPC_CameraEffects) for real-time parameter updates.
- The stress FOV pulse creates a subtle "unease" effect — pair with audio breathing changes for best results.
- For multiplayer: camera shakes are local-only; state changes replicate for shared visual consistency.
- Custom camera states can be added by extending E_CameraState and adding a config entry.
---
*Blueprint Spec: Camera State Layer. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.*

View File

@@ -0,0 +1,335 @@
# 15 — Player Metrics Tracker (`BPC_PlayerMetricsTracker`)
## Purpose
Records and exposes player behaviour telemetry throughout a play session. Metrics feed into the adaptive difficulty system, achievement tracking, end-game stats screen, and narrative pacing adjustments. This is a pure data-gathering component — it never blocks or modifies gameplay directly.
## Dependencies
- **Requires:** `GI_GameFramework` (session start/end events), `DA_GameDifficulty` (adaptive thresholds)
- **Required By:** `BPC_AdaptiveDifficulty` (consumes metrics), `WBP_StatsScreen` (end-game display), `GM_CoreGameMode` (death/checkpoint metrics)
- **Engine/Plugin Requirements:** `FTimerHandle` for play time tracking
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `ActorComponent` |
| **Class Type** | Blueprint Component |
| **Asset Path** | `Content/Framework/Player/BPC_PlayerMetricsTracker` |
| **Implements Interfaces** | `I_Persistable` (save/load metrics) |
---
## 1. Enums
### `E_MetricEventType`
| Value | Description |
|-------|-------------|
| `Death = 0` | Player died |
| `HideStarted = 1` | Player entered a hide spot |
| `HideEnded = 2` | Player exited a hide spot |
| `ItemCollected = 3` | Player picked up an item |
| `EnemyEncountered = 4` | Player entered an enemy detection range |
| `EnemyEvaded = 5` | Player escaped enemy detection |
| `StressPeakReached = 6` | Stress tier exceeded Distressed |
| `AreaExplored = 7` | Player entered a new area/chapter |
| `NarrativeEvent = 8` | Story beat triggered |
| `CombatEvent = 9` | Player used a weapon or was attacked |
---
## 2. Structs
### `S_BehaviourEvent`
| Field | Type | Description |
|-------|------|-------------|
| `Timestamp` | `Float` | Game time in seconds when event occurred |
| `EventType` | `E_MetricEventType` | What happened |
| `ContextTag` | `GameplayTag` | Tagged context for filtering |
| `FloatValue` | `Float` | Numeric payload (e.g. distance, intensity) |
| `StringValue` | `String` | Text payload (e.g. location name, enemy ID) |
| `Location` | `Vector` | World location where event occurred |
### `S_MetricsSnapshot`
| Field | Type | Description |
|-------|------|-------------|
| `TotalPlayTime` | `Float` | Total seconds played |
| `TimesDied` | `Integer` | Total deaths this session |
| `TimesHid` | `Integer` | Total hide events |
| `TotalDistanceWalked` | `Float` | Cumulative walking distance (cm) |
| `TotalDistanceSprinted` | `Float` | Cumulative sprinting distance (cm) |
| `ItemsCollected` | `Integer` | Total items picked up |
| `UniqueItemsCollected` | `Set<FName>` | Names of unique items collected |
| `EnemiesEncountered` | `Integer` | Total unique enemy encounters |
| `EnemiesEvaded` | `Integer` | Total successful evasions |
| `PeakStressTier` | `Float` | Highest stress tier reached (as float) |
| `StressPeakCount` | `Integer` | Times stress exceeded Distressed threshold |
| `NarrativeBeatsCompleted` | `Integer` | Story triggers activated |
| `AreasExplored` | `Set<FName>` | Unique areas/chapters visited |
| `CombatEvents` | `Integer` | Total combat interactions |
### `S_TimeSegment`
| Field | Type | Description |
|-------|------|-------------|
| `SegmentLabel` | `String` | Label for this time segment |
| `StartTime` | `Float` | Game time at segment start |
| `EndTime` | `Float` | Game time at segment end |
| `StressAverage` | `Float` | Average stress during segment |
| `MetricsDelta` | `S_MetricsSnapshot` | Metrics accumulated in this segment |
---
## 3. Variables
### Configuration (Instance Editable, Expose On Spawn)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `bTrackDetailedEvents` | `Boolean` | `true` | `Metrics Config` | If true, store full `S_BehaviourEvent` history |
| `MaxEventHistory` | `Integer` | `5000` | `Metrics Config` | Cap on stored events to prevent memory bloat |
| `bAutoSaveMetrics` | `Boolean` | `true` | `Metrics Config` | Auto-save metrics via I_Persistable on quit |
| `SnapshotInterval` | `Float` | `60.0` | `Metrics Config` | Seconds between automatic snapshot writes |
### Computed / Runtime (Public, Blueprint Read Only)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `CurrentSnapshot` | `S_MetricsSnapshot` | `-` | `Runtime Metrics` | Live accumulated metrics this session |
| `bIsTrackingSession` | `Boolean` | `false` | `Runtime Metrics` | Whether currently in a tracked session |
| `TimeSegmentHistory` | `Array<S_TimeSegment>` | `[]` | `Runtime Metrics` | Historical segments for analysis |
| `CurrentSegment` | `S_TimeSegment` | `-` | `Runtime Metrics` | Active time segment |
### Internal (Private / Protected, No Expose)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `SessionStartTime` | `Float` | `0.0` | `Timing` | Game time when session started |
| `CachedEventHistory` | `Array<S_BehaviourEvent>` | `[]` | `History` | Full event history (if tracking enabled) |
| `LastPosition` | `Vector` | `(0,0,0)` | `Tracking` | Previous frame position for distance calc |
| `SnapshotTimerHandle` | `FTimerHandle` | `-` | `Timing` | Timer for automatic snapshots |
### Replicated (if multiplayer)
| Variable | Type | Condition | Description |
|----------|------|-----------|-------------|
| `CurrentSnapshot` | `S_MetricsSnapshot` | `Replicated` | Synced session metrics |
---
## 4. Functions
### Public Functions
#### `StartSession` → `void`
- **Description:** Begins a new metrics tracking session. Resets snapshot.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `SegmentLabel` | `String` | Label for initial segment (e.g. "Chapter 1") |
- **Blueprint Authority:** Any
- **Flow:**
1. Set SessionStartTime = Current Game Time
2. Reset CurrentSnapshot to zero values
3. Initialize CurrentSegment with label and start time
4. Set bIsTrackingSession = true
5. Start snapshot timer
6. Fire OnSessionStarted
#### `EndSession` → `S_MetricsSnapshot`
- **Description:** Ends the tracking session and returns final snapshot.
- **Parameters:** None
- **Flow:**
1. Finalize CurrentSegment end time
2. Finalize CurrentSnapshot.PlayTime
3. Add CurrentSegment to TimeSegmentHistory
4. Set bIsTrackingSession = false
5. Clear snapshot timer
6. Fire OnSessionEnded
7. Return CurrentSnapshot
#### `RecordEvent` → `void`
- **Description:** Records a single behaviour event into history and updates snapshot.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `EventType` | `E_MetricEventType` | Type of event |
| `ContextTag` | `GameplayTag` | Filtering tag |
| `FloatValue` | `Float` | Numeric payload |
| `StringValue` | `String` | Text payload |
- **Flow:**
1. If not bIsTrackingSession: return
2. Create S_BehaviourEvent with current timestamp
3. If bTrackDetailedEvents and event count < MaxEventHistory:
- Add event to CachedEventHistory
4. Update CurrentSnapshot based on EventType:
- Death: TimesDied++
- HideStarted: TimesHid++
- ItemCollected: ItemsCollected++, add name to UniqueItemsCollected
- EnemyEncountered: EnemiesEncountered++
- EnemyEvaded: EnemiesEvaded++
- StressPeakReached: StressPeakCount++, update PeakStressTier if higher
- NarrativeEvent: NarrativeBeatsCompleted++
- AreaExplored: add to AreasExplored
- CombatEvent: CombatEvents++
5. Fire OnEventRecorded
#### `RecordDistanceTraveled` → `void`
- **Description:** Called by movement system or tick to log distance.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `DistanceDelta` | `Float` | Distance moved this frame |
| `bWasSprinting` | `Boolean` | Whether player was sprinting |
- **Flow:**
1. If bWasSprinting: CurrentSnapshot.TotalDistanceSprinted += Delta
2. Else: CurrentSnapshot.TotalDistanceWalked += Delta
#### `GetMetricsSnapshot` → `S_MetricsSnapshot`
- **Description:** Returns a copy of the current live snapshot.
- **Parameters:** None
- **Flow:** Return CurrentSnapshot
#### `GetEventHistory` → `Array<S_BehaviourEvent>`
- **Description:** Returns recorded event history.
- **Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `bFiltered` | `Boolean` | If true, apply filter |
| `FilterType` | `E_MetricEventType` | Only return events of this type |
- **Flow:**
1. If bFiltered: return filtered CachedEventHistory by FilterType
2. Else: return CachedEventHistory
#### `GetPlayTimeFormatted` → `String`
- **Description:** Returns total play time as formatted string (HH:MM:SS).
- **Parameters:** None
- **Flow:** Format SecondsToTimeString(SessionDuration)
### Protected / Private Functions
#### `OnTickDistance (Tick)` → `void`
- **Description:** Each tick, compute distance from LastPosition. Only active when tracking.
- **Flow:**
1. If not bIsTrackingSession: return
2. Get current actor location
3. Delta = Distance(LastPosition, CurrentLocation)
4. If Delta < 10.0: skip (noise threshold)
5. Check movement mode from BPC_MovementStateSystem
6. Call RecordDistanceTraveled(Delta, bWasSprinting)
7. Update LastPosition = CurrentLocation
#### `CreateSnapshot` → `void`
- **Description:** Timer callback to create periodic snapshots.
- **Flow:**
1. Copy CurrentSnapshot as snapshot
2. Finalize CurrentSegment end time
3. Store snapshot delta in CurrentSegment.MetricsDelta
4. Add CurrentSegment to TimeSegmentHistory
5. Start new CurrentSegment
---
## 5. Event Dispatchers
| Dispatcher | Parameters | Bind Access | Description |
|------------|-----------|-------------|-------------|
| `OnSessionStarted` | `String SegmentLabel` | `Public` | Fired when a tracking session begins |
| `OnSessionEnded` | `S_MetricsSnapshot FinalSnapshot` | `Public` | Fired when a tracking session ends |
| `OnEventRecorded` | `S_BehaviourEvent Event` | `Public` | Fired each time an event is recorded |
| `OnMetricsThresholdReached` | `E_MetricEventType EventType`, `int CurrentValue`, `int Threshold` | `Public` | Fired when a metric crosses a threshold |
| `OnSnapshotCreated` | `S_TimeSegment Segment` | `Public` | Fired when a periodic snapshot is written |
---
## 6. Overridden Events / Custom Events
### Event: `BeginPlay`
- **Description:** Cache owner reference, start session if game has started.
- **Flow:**
1. Get owning pawn
2. Bind to GI_GameFramework.OnGamePhaseChanged
3. If game phase is Playing: StartSession("Initial")
### Custom Event: `OnGamePhaseChangedHandler`
- **Description:** Auto-start/stop session based on game phase.
- **Flow:**
1. If NewPhase == Playing: StartSession("Resume")
2. If NewPhase == Menu / Transition / Ended: EndSession()
---
## 7. Blueprint Graph Logic Flow
```mermaid
flowchart TD
A[RecordEvent called] --> B{Is session active?}
B -->|No| C[Return]
B -->|Yes| D[Build S_BehaviourEvent]
D --> E{Has history space?}
E -->|Yes| F[Append to CachedEventHistory]
E -->|No| G[Skip history store]
G --> H[Update CurrentSnapshot counters]
F --> H
H --> I{Threshold crossed?}
I -->|Yes| J[Fire OnMetricsThresholdReached]
I -->|No| K[Fire OnEventRecorded]
J --> K
L[Tick: RecordDistanceTraveled] --> M[Get current location]
M --> N[Distance from LastPosition]
N --> O{Delta > 10 cm?}
O -->|No| P[Skip]
O -->|Yes| Q[Update snapshot distance]
Q --> R[Set LastPosition]
S[SnapshotTimer fires] --> T[Copy CurrentSnapshot]
T --> U[Write segment to history]
U --> V[Fire OnSnapshotCreated]
```
---
## 8. Communication Matrix
| Who Talks | How | What Is Sent |
|-----------|-----|-------------|
| `BPC_PlayerMetricsTracker` | `Direct call (other)` | `GetMetricsSnapshot` -> `BPC_AdaptiveDifficulty` |
| `BPC_PlayerMetricsTracker` | `Dispatcher` | `OnEventRecorded` -> `BPC_AdaptiveDifficulty` (real-time feed) |
| `BPC_PlayerMetricsTracker` | `Dispatcher` | `OnSessionEnded` -> `WBP_StatsScreen` (end-game display) |
| `BPC_PlayerMetricsTracker` | `Dispatcher` | `OnMetricsThresholdReached` -> `GM_CoreGameMode` (achievements) |
| `BPC_HealthSystem` | `Direct` | Calls `RecordEvent(Death)` on player death |
| `BPC_HidingSystem` | `Direct` | Calls `RecordEvent(HideStarted/HideEnded)` |
| `BPC_StressSystem` | `Direct` | Calls `RecordEvent(StressPeakReached)` |
| `GI_GameFramework` | `Dispatcher` | `OnGamePhaseChanged` -> `BPC_PlayerMetricsTracker` (start/stop) |
| `BPC_NarrativeManager` | `Direct` | Calls `RecordEvent(NarrativeEvent)` on story beat |
| `BPC_WeaponSystem` | `Direct` | Calls `RecordEvent(CombatEvent)` on attack |
| `BPC_InventoryManager` | `Direct` | Calls `RecordEvent(ItemCollected)` on pickup |
---
## 9. Validation / Testing Checklist
- [ ] StartSession resets all counters to zero
- [ ] Distance tracking correctly accumulates walking vs sprinting
- [ ] Events are capped at MaxEventHistory and do not overflow
- [ ] Snapshot timer creates periodic snapshots at correct interval
- [ ] EndSession returns accurate final snapshot
- [ ] Filtered event history returns only matching event types
- [ ] Threshold dispatchers fire at correct metric values
- [ ] I_Persistable save/load preserves snapshot across sessions
- [ ] Edge case: Rapid events within one frame all recorded
- [ ] Edge case: Session ends with zero events — snapshot is valid zero-state
- [ ] Edge case: Distance tracking ignores teleportation (delta > 5000 cm)
- [ ] Formatted play time displays correctly for all durations
---
## 10. Reuse Notes
- The metrics snapshot is designed to be consumed by `BPC_AdaptiveDifficulty` for dynamic difficulty adjustment.
- Event history can be serialised to disk via `I_Persistable` for cross-session analytics.
- The same `BPC_PlayerMetricsTracker` can be attached to any character blueprint — not just the player.
- Threshold values are not defined here; they live in `DA_GameDifficulty` or `DA_AdaptiveSettings`.
- For debugging, call `GetEventHistory(false)` to dump all events to log.
---
*Blueprint Spec: Player Metrics Tracker. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.*