# 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` | `[]` | `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` | `{}` | `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` | `{}` | `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.*