Files
UE5-Modular-Game-Framework/docs/blueprints/02-player/09_BPC_StaminaSystem.md

372 lines
16 KiB
Markdown

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