# 09 — Stamina System (`BPC_StaminaSystem`)

> **⚡ C++ Status: Stub** — `Source/PG_Framework/Public/Player/BPC_StaminaSystem.h` provides the UCLASS shell, `MaxStamina`/`CurrentStamina` variables, and `OnExhaustionStateChanged` dispatcher. **The C++ stub has NO gameplay logic.** **Create a BP child** and build ALL logic: sprint drain, action costs, exhaustion state machine, regen delay + rate, `CanAffordAction(Cost)` query. See `docs/developer/cpp-integration-guide.md`.
>
> ---

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

---

## 11. Multiplayer Networking (Expanded)

### Server RPCs

| RPC | Direction | Description |
|-----|-----------|-------------|
| `Server_DrainStamina` | Client→Server | Client requests stamina drain for action. Server validates MinStamina, applies. |
| `Server_StartContinuousDrain` | Client→Server | Client starts sprinting. Server begins drain timer. |
| `Server_StopContinuousDrain` | Client→Server | Client stops sprinting. Server stops drain, begins regen. |
| `Server_RestoreStamina` | Client→Server | Client uses stamina item. Server validates item, applies restore. |

### Authority Gates

```
Function DrainStamina(ActionType)
  If HasAuthority:
    → Check cooldown, MinStamina
    → Apply drain, update exhaustion, fire dispatchers
  Else:
    → Client predicts stamina bar decrease for responsiveness
    → Server_ RPC called; server corrects via OnRep if wrong

Function SetMaxStamina(NewMax, bMaintainRatio)
  If NOT HasAuthority → Return
  → Apply change, fire dispatchers, auto-replicates
```

### Client Prediction

- **Stamina bar:** Client predicts decrease on sprint/jump. Server corrects via `OnRep_CurrentStamina`.
- **Exhaustion:** ExhaustionState replicated. Client plays breathing audio locally.
- **Cooldowns:** Server-authoritative. Client shows cooldown timer after server confirms action.
- 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.*