add blueprints
This commit is contained in:
340
docs/blueprints/02-player/08_BPC_HealthSystem.md
Normal file
340
docs/blueprints/02-player/08_BPC_HealthSystem.md
Normal 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.*
|
||||
Reference in New Issue
Block a user