# 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` | `[]` | `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` | `{}` | `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. --- ## 11. Multiplayer Networking (Expanded) ### Replicated Variables (Existing + New) | Variable | Type | Condition | Description | |----------|------|-----------|-------------| | `CurrentHealth` | `Float` | `Replicated Using OnRep_CurrentHealth` | Clients sync via OnRep → dispatcher | | `DeathState` | `E_DeathState` | `Replicated Using OnRep_DeathState` | Death state synced; OnRep triggers death effects | | `bIsInvincible` | `Boolean` | `Replicated` | Invincibility visible to other players | | `MaxHealth` | `Float` | `Replicated` | Synced for UI percentage calculations | ### Server RPCs | RPC | Direction | Description | |-----|-----------|-------------| | `Server_ApplyDamage` | Client→Server | Client reports taking damage. Server validates source, range, damage type. | | `Server_ApplyHealing` | Client→Server | Client requests healing. Server validates heal source, applies. | | `Server_KillInstant` | Client→Server | Only accepted from authoritative sources (GM, traps). Client calls are rejected. | ### Authority Gates ``` Function ApplyDamage(DamageEvent) Switch HasAuthority Authority: → Calculate resistance, apply damage, fire dispatchers Remote: → Return (clients never modify health directly) → Server_ApplyDamage is called by damage source (weapon/trap), not victim Function ApplyHealing(HealAmount) Switch HasAuthority Authority: → Apply healing, fire dispatchers Remote: → Return → Consumable use calls Server_UseConsumable → server calls ApplyHealing ``` ### Client Prediction - **Health bar:** Client predicts damage flash on hit; server-corrected value arrives via `OnRep_CurrentHealth`. - **Death:** No prediction. `OnRep_DeathState` triggers all death effects (screen fade, ragdoll). - **Healing:** Client shows green flash immediately + predicted health; server corrects within one RTT. - **Invincibility:** Visual effect (shield shimmer) is local cosmetic; state is replicated. ### OnRep Handlers ``` OnRep_CurrentHealth() → Broadcast OnHealthChanged(OldHealth, CurrentHealth, Delta) → UI updates identically to single-player path OnRep_DeathState() → Broadcast OnDeathStateChanged(OldState, NewState) → If Dead: play death animation, screen effects, notify GM ``` ### Anti-Cheat - Server validates all `Server_ApplyDamage` calls: check instigator range, weapon fire rate, damage type validity. - Server never trusts `DamageEvent.BaseAmount` from client — always recalculates from weapon data. - `Server_KillInstant` is only callable from server-authoritative systems (traps, death zones, GM). --- *Blueprint Spec: Health System. Conforms to TEMPLATE.md v1.0 — part of the UE5 Modular Game Framework.*