# 130 — Central State Authority (`BPC_StateManager`) ## Purpose Single source of truth for "what can the player do right now?" Every system queries `IsActionPermitted(Tag)` instead of checking other systems' states directly. Manages exclusive action states, upper-body overlay states, action action gating, vital signs (heart rate), and the force-stack pattern for nested overrides (death, cutscenes, void space). Communicates with GASP exclusively through overlay state variables — never touches motion matching internals. ## Dependencies - **Requires:** `GI_GameFramework` (04 — game phase changes), `BPC_HealthSystem` (08 — death + injury bindings), `BPC_StressSystem` (10 — stress tier), `BPC_StaminaSystem` (09 — exhaustion), `BPC_MovementStateSystem` (11 — movement mode tracking), `GI_GameTagRegistry` (01 — tag queries), `SS_EnhancedInputManager` (128 — context switching), `ABP_GASP` (external — overlay + intensity variables) - **Required By:** EVERY gameplay system (central query point for action permission) - **Engine/Plugin Requirements:** GameplayTags plugin, GASP plugin (read-only) ## Class Info | Property | Value | |----------|-------| | **Parent Class** | `ActorComponent` | | **Class Type** | Blueprint Component | | **Asset Path** | `Content/Framework/Player/BPC_StateManager.uasset` | | **Implements Interfaces** | `I_AnimNotifyInterface` (receives GASP notifies) | --- ## 1. Enums ### E_PlayerActionState — Exclusive Action States (45 values) ```text Enum Name: E_PlayerActionState (DisplayName = "Player Action State") Values: Idle = 0 // Standing, not performing any action Walking = 1 // Moving at walk speed Jogging = 2 // Moving at jog speed Sprinting = 3 // Moving at sprint speed (stamina draining) Crouching = 4 // Crouched posture, possibly moving Crawling = 5 // Prone, crawling Sitting = 6 // Sitting on surface — immobile, can look around Vaulting = 7 // Transient — vaulting over obstacle Climbing = 8 // Ledge or ladder climbing Sliding = 9 // Transient — sliding under or down Interacting = 10 // Performing a hold/press interaction Inspecting = 11 // Inspecting an item in close-up view InInventory = 12 // Inventory menu is open InJournal = 13 // Journal/document viewer is open InPauseMenu = 14 // Pause menu is open InDialogue = 15 // In dialogue with an NPC InCutscene = 16 // Pre-authored cutscene playing SwingMelee = 17 // Performing a melee attack AimingFirearm = 18 // Aiming a firearm FiringFirearm = 19 // Firing a firearm (transient) Reloading = 20 // Reloading a firearm DeployingShield = 21 // Raising a shield Hiding = 22 // Inside a hiding spot (enclosed/behind/under/shadow) Peeking = 23 // Peeking from behind cover GrabbingObject = 24 // Holding a physics object UsingObject = 25 // Using a world object (lever, panel, etc.) Dead = 26 // Dead — death animation playing or death loop active InVoidSpace = 27 // In alt death / void space InPuzzle = 28 // Using a puzzle device InTrialScenario = 29 // In a timed trial scenario InMainMenu = 30 // At main menu (no gameplay) Loading = 31 // Loading screen active DraggingCorpse = 32 // Dragging a physics body HoldingBreath = 33 // Holding breath while hiding Injured = 34 // Health ≤ threshold — reduced speed, limp anim, restricted actions Staggered = 35 // Transient — hit reaction / stagger KnockedDown = 36 // Knocked to ground Exhausted = 37 // Stamina fully depleted — panting/immobile Panicked = 38 // Stress at catatonic tier — stumble/freeze PushingObject = 39 // Pushing a movable object AimingThrowable = 40 // Aiming a throwable item ThrowingItem = 41 // Throwing an item (transient) UsingConsumable = 42 // Using a consumable (heal, inject, etc.) InPhotoMode = 43 // Photo mode active — free cam, no gameplay input CustomAction = 44 // Reserved for mod/expansion actions ``` ### E_OverlayState — Upper-Body Overlay for GASP AnimBP (18 values) ```text Enum Name: E_OverlayState (DisplayName = "Upper Body Overlay State") Values: Empty = 0 // No overlay — default locomotion Flashlight = 1 // Holding flashlight Melee_OneHanded = 2 // One-handed melee weapon (knife, baton) Melee_TwoHanded = 3 // Two-handed melee weapon (axe, pipe) Pistol = 4 // Pistol / handgun Shotgun = 5 // Shotgun Rifle = 6 // Rifle / carbine Shield = 7 // Shield only Shield_And_Melee = 8 // Shield + one-handed melee Shield_And_Pistol = 9 // Shield + pistol DualWield = 10 // Dual-wielding one-handed weapons Inspect = 11 // Inspecting an object DualHand_Flashlight_Melee = 12 // Flashlight + melee Injured = 13 // Injured arm (lowered, favoring) Throwable = 14 // Holding a throwable (grenade, bottle) Consumable = 15 // Using consumable (syringe, bandage) TwoHanded_Heavy = 16 // Heavy two-handed (sledgehammer, chainsaw) EmptyHands_Raised = 17 // Empty hands raised (surrender, blocking bare) ``` ### E_ActionRequestResult — Result Enum for RequestStateChange (8 values) ```text Enum Name: E_ActionRequestResult Values: Permitted = 0 // State change approved Blocked_CurrentState = 1 // Blocked because player is in an incompatible state Blocked_GamePhase = 2 // Blocked by game phase (menu, cutscene, loading) Blocked_Dead = 3 // Player is dead Blocked_Stamina = 4 // Insufficient stamina Blocked_Equipment = 5 // Missing required equipment Blocked_Cooldown = 6 // Action on cooldown Blocked_Custom = 7 // Blocked by a custom gating rule ``` ### E_PlayerVitalSignals — Vital Signal Enum for HeartRate / Breathing (5 values) ```text Enum Name: E_PlayerVitalSignals (DisplayName = "Player Vital Signals") Values: Normal = 0 // Heart rate 60-80 BPM — resting/exploring Elevated = 1 // Heart rate 80-110 BPM — sprinting, light combat High = 2 // Heart rate 110-150 BPM — heavy combat, chased Critical = 3 // Heart rate 150+ BPM — near death, terrified Erratic = 4 // Irregular rhythm — panic state, hallucinating ``` --- ## 2. Structs ### `S_StateChangeRequest` | Field | Type | Description | |-------|------|-------------| | `RequestedState` | `E_PlayerActionState` | The state the requester wants to enter | | `RequesterTag` | `GameplayTag` | Identifies which system is requesting | | `Priority` | `Integer` | 0=Normal, 50=High, 100=Force (bypasses all gating) | | `OverrideReason` | `Text` | Human-readable reason for override/force | ### `S_StateGatingRule` | Field | Type | Description | |-------|------|-------------| | `ActionTag` | `GameplayTag` | The action being gated | | `BlockedStates` | `Array` | States that block this action | | `BlockedGamePhases` | `Array` | Game phases that block this | | `RequiredTags` | `GameplayTagContainer` | Tags the player must have | | `BlockedTags` | `GameplayTagContainer` | Tags the player must NOT have | | `BlockReason` | `Text` | Reason shown to player when blocked | | `Priority` | `Integer` | Rule evaluation order (lower = checked first) | ### `S_ActionPermissionResult` | Field | Type | Description | |-------|------|-------------| | `bPermitted` | `Boolean` | Whether the action is allowed | | `BlockReason` | `Text` | Why it was blocked (if not permitted) | | `ResultCode` | `E_ActionRequestResult` | Machine-readable result code | --- ## 3. Variables ### Configuration (Instance Editable, Expose On Spawn) | Variable | Type | Default | Category | Description | |----------|------|---------|----------|-------------| | `GatingRules` | `Array` | `DefaultRules` | `State Config` | All gating rules defined in blueprint | | `DefaultState` | `E_PlayerActionState` | `Idle` | `State Config` | State to initialize on BeginPlay | | `DefaultOverlay` | `E_OverlayState` | `Empty` | `State Config` | Default overlay state | | `StateTransitionDelay` | `Float` | `0.0` | `State Config` | Minimum time between state changes (anti-spam) | | `InjuryHealthThreshold` | `Float` | `0.30` | `State Config` | Health ratio (0-1) below which player becomes Injured | | `InjurySpeedMultiplier` | `Float` | `0.65` | `State Config` | Walk speed multiplier when injured | | `HeartRateBase` | `Float` | `70.0` | `State Config` | Base heart rate in BPM when calm | | `HeartRateMax` | `Float` | `180.0` | `State Config` | Maximum heart rate in BPM | | `StateGatingTable` | `DA_StateGatingTable` | `None` | `State Config` | External Data Asset for designer-tunable gating rules | | `bLogStateTransitions` | `Boolean` | `false` | `Debug` | Log all state transitions to console | ### Internal (Private / Protected, No Expose) | Variable | Type | Default | Category | Description | |----------|------|---------|----------|-------------| | `CurrentState` | `E_PlayerActionState` | `Idle` | `State` | Current exclusive action state | | `PreviousState` | `E_PlayerActionState` | `Idle` | `State` | Previous state for transitions | | `CurrentOverlay` | `E_OverlayState` | `Empty` | `State` | Current upper-body overlay state | | `LastStateChangeTime` | `Float` | `0.0` | `State` | Game time of last state change | | `StateHistory` | `Array` | `Empty` | `State` | History of last N states for debugging | | `ForceStack` | `Array` | `Empty` | `State` | Stack of forced states for nested overrides | | `bIsTransitioning` | `Boolean` | `false` | `State` | True during a state transition | | `PendingRequests` | `Array` | `Empty` | `State` | Queued requests during transition | | `CachedABPRef` | `ABP_GASP` (soft ref) | `None` | `State` | Cached reference to GASP AnimBP | | `CurrentActionFlags` | `Map` | `Empty` | `State` | Active action tags with duration/timestamp | | `CurrentHeartRate` | `Float` | `70.0` | `Vital Signs` | Current heart rate in BPM (recalculated each tick, smoothed) | | `CurrentVitalSignal` | `E_PlayerVitalSignals` | `Normal` | `Vital Signs` | Current vital signal category | | `bIsInjured` | `Boolean` | `false` | `Vital Signs` | True when health ≤ InjuryHealthThreshold | --- ## 4. Functions ### Public Functions #### `RequestStateChange` → `E_ActionRequestResult` - **Description:** The primary entry point. Any system requests to transition the player to a new exclusive action state. State Manager checks all gating rules and returns the result. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `NewState` | `E_PlayerActionState` | The desired new action state | | `RequesterTag` | `GameplayTag` | Identifies which system is requesting | | `Priority` | `Integer` (default 0) | 0=Normal, 50=High, 100=Force | - **Blueprint Authority:** Any - **Flow:** 1. If Priority == 100 (Force): bypass gating, go directly to state transition 2. If `bIsTransitioning`: queue the request on `PendingRequests`, return `Blocked_CurrentState` 3. Check time since `LastStateChangeTime` — if < `StateTransitionDelay`: return `Blocked_Cooldown` 4. If `CurrentState` == `Dead` and `NewState` != `Idle` (respawn): return `Blocked_Dead` 5. Fire `OnStateChangeRequested` 6. Iterate `GatingRules` matching `RequesterTag`: - If `NewState` is in `BlockedStates` for any rule → fire `OnStateChangeDenied`, return `Blocked_CurrentState` with `BlockReason` - If `CurrentGamePhase` is in `BlockedGamePhases` → return `Blocked_GamePhase` - If required tags missing or blocked tags present → return appropriate result 7. Execute state transition (set PreviousState, set CurrentState, update GASP, fire dispatchers) 8. Return `Permitted` #### `ForceStateChange` → `void` - **Description:** Bypasses ALL gating. Used by death system, cutscene bridge, void space entry, and developer cheats. Pushes current state onto `ForceStack`. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `NewState` | `E_PlayerActionState` | The forced state to enter | | `Reason` | `Text` | Human-readable reason for force | - **Blueprint Authority:** Any (Server-authoritative if multiplayer) - **Flow:** 1. If `ForceStack` already has `NewState` at top: return (prevents double-force) 2. Push `CurrentState` onto `ForceStack` 3. Fire `OnForceOverridePushed` 4. Execute state transition with `NewState` 5. Log reason to console if `bLogStateTransitions` #### `RestorePreviousState` → `void` - **Description:** Pops the force stack. Used when exiting cutscenes, void space, or force-override states. - **Parameters:** *(none)* - **Blueprint Authority:** Any - **Flow:** 1. If `ForceStack` is empty: set to `Idle` or previously known normal state, return 2. Pop top of `ForceStack` 3. Fire `OnForceOverridePopped` 4. Execute state transition to popped state #### `GetCurrentState` → `E_PlayerActionState` - **Description:** Pure getter — returns `CurrentState`. No side effects. - **Parameters:** *(none)* - **Blueprint Authority:** Any (Pure) #### `GetPreviousState` → `E_PlayerActionState` - **Description:** Pure getter — returns `PreviousState`. No side effects. - **Parameters:** *(none)* - **Blueprint Authority:** Any (Pure) #### `IsActionPermitted` → `S_ActionPermissionResult` - **Description:** Non-mutating query. Does NOT change state. Any system can ask "can I do this right now?" - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `ActionTag` | `GameplayTag` | The action tag to check | - **Blueprint Authority:** Any (Pure) - **Flow:** 1. Find all `GatingRules` where `ActionTag` matches 2. Check `CurrentState` against `BlockedStates` 3. Check `GI_GameFramework.CurrentGamePhase` against `BlockedGamePhases` 4. Check `RequiredTags` and `BlockedTags` 5. Return `S_ActionPermissionResult` with `bPermitted`, `BlockReason`, `ResultCode` #### `SetOverlayState` → `void` - **Description:** Tells GASP AnimBP which upper-body overlay to use. Does NOT change `E_PlayerActionState`. Called by equipment system, weapon system, and inspection. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `Overlay` | `E_OverlayState` | The new overlay state | - **Blueprint Authority:** Any - **Flow:** 1. If `Overlay` == `CurrentOverlay`: return (no-op) 2. Set `CurrentOverlay` = `Overlay` 3. If `CachedABPRef` is valid: set `ABP_GASP.OverlayState` = `Overlay` 4. Fire `OnOverlayStateChanged` 5. Update visible first-person arm mesh via `BPC_EmbodimentSystem` #### `CanTransitionTo` → `S_ActionPermissionResult` - **Description:** Pre-flight check without executing. Used by UI to enable/disable buttons. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `TargetState` | `E_PlayerActionState` | The state to check transition eligibility for | - **Blueprint Authority:** Any (Pure) #### `UpdateHeartRate` → `void` - **Description:** Recalculates heart rate each tick based on current state, stress, stamina, and health. Evaluates `E_PlayerVitalSignals` thresholds. Called automatically from Tick. - **Parameters:** *(none — reads internal state and bound systems)* - **Blueprint Authority:** Internal (Tick-driven) - **Flow:** 1. Calculate base rate from current state: - Idle/Walking/Sitting → `HeartRateBase` (60-80) - Jogging → Base + 15 - Sprinting → Base + 30 - Combat (any weapon state) → Base + 40 - Hiding/Peeking → Base + 5 (suppressed by stealth) - Panicked → Base + 50 - Dead → 0 2. Apply health modifier: (1.0 - HealthRatio) * 20 added 3. Apply stress modifier: StressRatio * 30 added 4. Apply stamina modifier: if Exhausted → +25 5. Clamp to `HeartRateMax` 6. Smooth interpolation: `CurrentHeartRate = FMath::FInterpTo(CurrentHeartRate, Target, DeltaTime, 3.0)` 7. Evaluate `E_PlayerVitalSignals` from `CurrentHeartRate`: - ≤ 80 → Normal - ≤ 110 → Elevated - ≤ 150 → High - ≤ 180 → Critical - If Panicked state OR FearSystem active → Erratic (overrides BPM threshold) 8. If `CurrentVitalSignal` changed, fire `OnVitalSignalChanged` 9. Write `CurrentHeartRate` to `ABP_GASP.HeartRate` for breathing animation blend #### `EvaluateInjuryState` → `void` - **Description:** Called when health changes. Sets `bIsInjured` flag and auto-transitions to/from `Injured` state if health crosses threshold. - **Parameters:** *(none — reads BPC_HealthSystem)* - **Blueprint Authority:** Internal (called by health change binding) - **Flow:** 1. Get `HealthRatio` from `BPC_HealthSystem.CurrentHealth / MaxHealth` 2. If `HealthRatio <= InjuryHealthThreshold` AND `!bIsInjured`: - Set `bIsInjured = true` - If CurrentState is Idle/Walking/Jogging/Sprinting/Crouching: `RequestStateChange(Injured, Tag.Health)` - Apply `InjurySpeedMultiplier` to movement - Fire `OnInjuryStateChanged(true, HealthRatio)` 3. If `HealthRatio > InjuryHealthThreshold` AND `bIsInjured`: - Set `bIsInjured = false` - If CurrentState == Injured: `RequestStateChange(Idle, Tag.Health)` - Restore movement speed - Fire `OnInjuryStateChanged(false, HealthRatio)` #### `IsInCombat` → `Boolean` - **Description:** Convenience — returns true if `CurrentState` is `SwingMelee`, `AimingFirearm`, `FiringFirearm`, `Reloading`, or `DeployingShield`. - **Parameters:** *(none)* - **Blueprint Authority:** Any (Pure) #### `IsMovementBlocked` → `Boolean` - **Description:** Convenience — returns true if player cannot move at all. True when `CurrentState` is `Dead`, `InCutscene`, `InDialogue`, `InInventory`, `InJournal`, `InPauseMenu`, `InMainMenu`, `UsingObject`, `Sitting`, `KnockedDown`, `Panicked`, `Staggered`, `Exhausted`, `Loading`, `InPhotoMode`. - **Parameters:** *(none)* - **Blueprint Authority:** Any (Pure) #### `IsMovementRestricted` → `Boolean` - **Description:** Convenience — returns true if movement is partially restricted (slowed/impaired but not fully blocked). True when `CurrentState` is `Injured`, `Crouching`, `Crawling`, `HoldingBreath`, `AimingFirearm`, `DeployingShield`, `Hiding`, `Peeking`, `GrabbingObject`, `PushingObject`, `UsingConsumable`. - **Parameters:** *(none)* - **Blueprint Authority:** Any (Pure) #### `GetHeartRate` → `Float` - **Description:** Returns `CurrentHeartRate` in BPM. Used by audio system for heartbeat SFX tempo, by UI for vitals display, by haptic system for controller pulse rate. - **Parameters:** *(none)* - **Blueprint Authority:** Any (Pure) #### `GetVitalSignal` → `E_PlayerVitalSignals` - **Description:** Returns `CurrentVitalSignal`. Used by atmosphere system (audio tension layers), camera (breathing sway), and narrative system (dialogue conditionals). - **Parameters:** *(none)* - **Blueprint Authority:** Any (Pure) #### `CanEquipWeapon` → `Boolean` - **Description:** Checks if weapon equip is permitted in current state. Convenience wrapper around `IsActionPermitted`. - **Parameters:** *(none)* - **Blueprint Authority:** Any (Pure) #### `RegisterActionFlag` → `void` - **Description:** Lightweight flag system for transient action permissions. Duration of -1 = permanent until unregistered. Fires `OnActionFlagRegistered`. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `Tag` | `GameplayTag` | The flag tag to register | | `Duration` | `Float` (default -1.0) | How long the flag persists (-1 = permanent) | #### `UnregisterActionFlag` → `void` - **Description:** Removes a previously registered action flag. Fires `OnActionFlagExpired` if the flag was active. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `Tag` | `GameplayTag` | The flag tag to remove | #### `HasActionFlag` → `Boolean` - **Description:** Returns true if the given tag is currently registered as an action flag. - **Parameters:** | Param | Type | Description | |-------|------|-------------| | `Tag` | `GameplayTag` | The flag tag to check | - **Blueprint Authority:** Any (Pure) --- ## 5. Event Dispatchers | Dispatcher | Parameters | Bind Access | Description | |------------|-----------|-------------|-------------| | `OnStateChanged` | `OldState: E_PlayerActionState`, `NewState: E_PlayerActionState` | `Public` | Fired on any state transition | | `OnStateChangeRequested` | `Request: S_StateChangeRequest` | `Public` | Fired when request is made (before evaluation) | | `OnStateChangeDenied` | `Request: S_StateChangeRequest`, `Result: E_ActionRequestResult`, `Reason: Text` | `Public` | Fired when a request is denied | | `OnOverlayStateChanged` | `OldOverlay: E_OverlayState`, `NewOverlay: E_OverlayState` | `Public` | Fired on overlay change | | `OnActionFlagRegistered` | `Tag: GameplayTag`, `Duration: Float` | `Public` | Fired when a flag is registered | | `OnActionFlagExpired` | `Tag: GameplayTag` | `Public` | Fired when a timed flag expires | | `OnForceOverridePushed` | `ForcedState: E_PlayerActionState`, `PoppedState: E_PlayerActionState` | `Public` | Fired when force stack is pushed | | `OnForceOverridePopped` | `RestoredState: E_PlayerActionState` | `Public` | Fired when force stack is popped | | `OnVitalSignalChanged` | `OldSignal: E_PlayerVitalSignals`, `NewSignal: E_PlayerVitalSignals` | `Public` | Fired when vital signal tier changes | | `OnHeartRateChanged` | `NewBPM: Float` | `Public` | Fired when heart rate changes significantly (±5 BPM) | | `OnInjuryStateChanged` | `bIsInjured: Boolean`, `HealthRatio: Float` | `Public` | Fired when injury state toggles | --- ## 6. Overridden Events / Custom Events ### Event: `BeginPlay` → `Initialize` - **Description:** Sets up all bindings, caches references, loads default state, and enables tick. - **Flow:** 1. Get owner as Character (validate — log error and disable if invalid) 2. Cache reference to `ABP_GASP` from character's skeletal mesh 3. Set `CurrentState` = `DefaultState`, `CurrentOverlay` = `DefaultOverlay` 4. Populate default `GatingRules` from `DA_StateGatingTable` if assigned 5. Bind to `GI_GameFramework.OnGamePhaseChanged` (for game-phase-gated rules) 6. Bind to `BPC_HealthSystem.OnDeath` (auto-call `ForceStateChange(Dead, "Death")`) 7. Register with `GI_GameTagRegistry` for tag-based queries 8. Bind to `BPC_HealthSystem.OnHealthChanged` → call `EvaluateInjuryState()` 9. Bind to `BPC_StressSystem.OnStressTierChanged` → recalculate heart rate 10. Bind to `BPC_StaminaSystem.OnExhaustionStateChanged` → recalculate heart rate 11. Enable component tick (required for heart rate smoothing) ### Event: `Tick` → `UpdateHeartRate` - **Description:** Called every tick. Recalculates heart rate and evaluates vital signals. - **Flow:** (see `UpdateHeartRate` function flow above) ### Event: `OnComponentDestroyed` - **Description:** Cleanup — unbind all bound delegates, null cached references. - **Flow:** 1. Unbind from `GI_GameFramework.OnGamePhaseChanged` 2. Unbind from `BPC_HealthSystem.OnDeath` and `OnHealthChanged` 3. Unbind from `BPC_StressSystem.OnStressTierChanged` 4. Unbind from `BPC_StaminaSystem.OnExhaustionStateChanged` 5. Set `CachedABPRef` = `None` --- ## 7. Blueprint Graph Logic Flow ### RequestStateChange Flow ```mermaid flowchart TD A[System calls RequestStateChange] --> B{Priority == 100?} B -->|Yes Force| C[ExecuteStateTransition] B -->|No| D{bIsTransitioning?} D -->|Yes| E[Queue on PendingRequests - return Blocked_CurrentState] D -->|No| F{Within StateTransitionDelay?} F -->|Yes| G[Return Blocked_Cooldown] F -->|No| H{CurrentState == Dead?} H -->|Yes and NewState != Idle| I[Return Blocked_Dead] H -->|No| J[Fire OnStateChangeRequested] J --> K[Evaluate GatingRules for RequesterTag] K --> L{NewState in BlockedStates?} L -->|Yes| M[Fire OnStateChangeDenied - return Blocked_CurrentState] L -->|No| N{GamePhase blocks?} N -->|Yes| O[Fire OnStateChangeDenied - return Blocked_GamePhase] N -->|No| P{Required tags missing?} P -->|Yes| Q[Fire OnStateChangeDenied - return reason] P -->|No| R{Blocked tags present?} R -->|Yes| Q R -->|No| C C --> S[PreviousState = CurrentState] S --> T[CurrentState = NewState] T --> U[Update bIsInAction on GASP] U --> V[Update ActionIntensity on GASP] V --> W[Fire OnStateChanged - OldState, NewState] W --> X[Notify SS_EnhancedInputManager - context change] X --> Y[Return Permitted] ``` ### Overlay State Pipeline ```mermaid sequenceDiagram participant ES as EquipmentSystem participant SM as BPC_StateManager participant GASP as ABP_GASP participant AP as AnimPose ES->>SM: SetOverlayState(Pistol) SM->>SM: CurrentOverlay = Pistol SM->>GASP: ABP_GASP.OverlayState = Pistol GASP->>GASP: Blend upper body to Pistol pose GASP->>AP: Apply overlay pose SM->>SM: Fire OnOverlayStateChanged SM->>ES: Confirm overlay set ``` --- ## 8. State Gating Reference Tables ### Complete Action → Blocking State Matrix | Requested Action (Tag) | Blocked If CurrentState Is | Reason | |------------------------|----------------------------|--------| | `Action.Move.Walk` | Dead, InCutscene, InDialogue, KnockedDown, Panicked, Exhausted, Sitting, InInventory, InJournal, InPauseMenu, UsingObject, GrabbingObject, InPhotoMode | Movement disabled | | `Action.Move.Sprint` | Dead, InCutscene, InDialogue, Crouching, Crawling, Hiding, Peeking, Sitting, KnockedDown, Exhausted, Panicked, Reloading, UsingConsumable, Injured | Sprint requires standing + uninjured | | `Action.Move.Crouch` | Dead, Vaulting, Climbing, Sliding, InCutscene, InDialogue, Sitting, KnockedDown, Exhausted, Panicked | Cannot crouch during traversal | | `Action.Move.Crawl` | Dead, Standing required states, Vaulting, Climbing, InCutscene, Sitting, KnockedDown | Must be prone first | | `Action.Move.Jump` | Dead, Hiding, Peeking, Crouching (maybe), Crawling, InCutscene, InDialogue, Sitting, KnockedDown, Panicked, Exhausted, UsingObject, InInventory, InPauseMenu | Jump blocked | | `Action.Move.Sit` | Dead, Vaulting, Climbing, Hiding, SwingMelee, FiringFirearm, Reloading, KnockedDown, Panicked, InInventory, InCutscene, InDialogue | Sit requires neutral standing state | | `Action.Move.StandUp` | Must be Sitting state | Must be sitting first | | `Action.Traversal.Vault` | Dead, Hiding, Peeking, InCutscene, InDialogue, Reloading, SwingMelee, FiringFirearm, KnockedDown, Panicked | Traversal requires free hands | | `Action.Traversal.Climb` | Dead, InCutscene, InDialogue, Reloading, SwingMelee, KnockedDown, Panicked | Climbing requires free hands | | `Action.Traversal.Slide` | Dead, InCutscene, InDialogue, Reloading, SwingMelee, KnockedDown, Panicked | Slide requires two hands | | `Action.Hide.Enter` | Dead, InCutscene, InDialogue, SwingMelee, FiringFirearm, Reloading, Vaulting, Climbing, KnockedDown, Panicked, Exhausted, GrabbingObject | Hide requires exposed state | | `Action.Hide.Peek` | Dead, SwingMelee, FiringFirearm, Reloading | Peek only from hidden | | `Action.Hide.Exit` | InCutscene, Dead (if not force-exit) | Forced exit allowed | | `Action.Interact.Hold` | Dead, InCutscene, SwingMelee, FiringFirearm, Reloading, Hiding, Peeking, Vaulting, Climbing, KnockedDown, Panicked, InInventory, InPauseMenu, InDialogue, UsingObject | Interaction requires free state | | `Action.Interact.Instant` | Dead, InCutscene, KnockedDown, Panicked | Instant interaction only blocked by critical | | `Action.Inventory.Open` | Dead, InCutscene, Vaulting, Climbing, SwingMelee, FiringFirearm, Reloading, KnockedDown, Panicked, Hiding, GrabbingObject, InDialogue, InPuzzle, InTrialScenario | Inventory blocked during actions | | `Action.Inventory.Close` | (always permitted — but may not open if blocked) | Close always works | | `Action.Inventory.Equip` | Dead, InCutscene, Vaulting, Climbing, KnockedDown, Panicked | Equip requires stability | | `Action.Inventory.UseItem` | Dead, InCutscene, Vaulting, Climbing, KnockedDown, Panicked, Hiding | Use requires stable state | | `Action.Weapon.Fire` | Dead, Hiding, Peeking, Reloading, Vaulting, Climbing, InCutscene, InDialogue, KnockedDown, Panicked, InInventory, InPauseMenu, UsingConsumable, GrabbingObject | Cannot fire in these states | | `Action.Weapon.Aim` | Dead, Hiding, Peeking, Vaulting, Climbing, InCutscene, InDialogue, KnockedDown, Panicked, InInventory, InPauseMenu, GrabbingObject | Cannot aim | | `Action.Weapon.Reload` | Dead, Hiding, Peeking, Vaulting, Climbing, SwingMelee, InCutscene, InDialogue, KnockedDown, Panicked, InPauseMenu, GrabbingObject | Cannot reload | | `Action.Weapon.Melee` | Dead, Hiding, Peeking, Reloading, Vaulting, Climbing, InCutscene, InDialogue, KnockedDown, Panicked, InInventory, InPauseMenu, GrabbingObject, UsingConsumable | Cannot melee | | `Action.Weapon.Block` | Dead, Reloading, Vaulting, Climbing, InCutscene, KnockedDown, Panicked, Hiding, InInventory, InPauseMenu | Cannot block | | `Action.Dialogue.Start` | Dead, InCutscene, Vaulting, Climbing, SwingMelee, FiringFirearm, Hiding, InInventory, InPauseMenu, Reloading, KnockedDown, Panicked | Dialogue requires neutral state | | `Action.Dialogue.Choose` | Must be InDialogue state | Must be in dialogue | | `Action.Cutscene.Play` | (handled by GI_GameFramework game phase, not action state) | Cutscene overrides via Force | | `Action.Cutscene.Skip` | Must be InCutscene, bCanSkip | Must be in cutscene | | `Action.HoldBreath` | Hiding state only, not Peeking | Breath hold only while hidden | | `Action.Grab.PhysicsObject` | Dead, InCutscene, Hiding, Vaulting, Climbing, Reloading, SwingMelee, FiringFirearm, KnockedDown, Panicked, InInventory, InPauseMenu | Grab requires free hands | | `Action.Puzzle.Use` | Dead, InCutscene, InDialogue, Vaulting, Climbing, Hiding, SwingMelee, FiringFirearm, Reloading, KnockedDown, Panicked | Puzzle requires focus | | `Action.Consumable.Use` | Dead, InCutscene, Vaulting, Climbing, Hiding, SwingMelee, FiringFirearm, Reloading, KnockedDown, Panicked, InInventory | Consumable requires free hands | | `Action.Pause.Open` | InCutscene, Dead, InMainMenu, Loading | Cannot pause in cutscene/death | | `Action.Pause.Close` | (always permitted) | Close always works | | `Action.Journal.Open` | Dead, InCutscene, InDialogue, Vaulting, Climbing, Hiding, SwingMelee, FiringFirearm, Reloading, KnockedDown, Panicked, InInventory, InPuzzle, InTrialScenario | Journal requires neutral state | | `Action.Settings.Open` | InCutscene, Loading | Settings limited during cutscene | | `Action.MainMenu.Return` | (always permitted) | Return to menu always works | | `Action.QuestLog.Open` | Same as Journal | Same blocking states | | `Action.Map.Open` | Dead, InCutscene, Vaulting, Climbing, Hiding, InInventory, InPauseMenu | Map in neutral/explore states | | `Action.Throwable.Aim` | Dead, Hiding, Peeking, Vaulting, Climbing, InCutscene, InDialogue, Reloading, KnockedDown, Panicked, InInventory, InPauseMenu | Throw requires free hands | | `Action.Throwable.Throw` | Same as Aim + must be in AimingThrowable state | Must be aiming first | | `Action.PhotoMode.Enter` | Dead, InCutscene, InDialogue, Vaulting, Climbing, Hiding, SwingMelee, FiringFirearm, KnockedDown, Panicked | Photo mode requires neutral state | | `Action.PhotoMode.Exit` | Must be InPhotoMode | Must be in photo mode | ### Restricted Movement States | State | Walk Allowed | Sprint Allowed | Jump Allowed | Crouch Allowed | Camera Freelook | Notes | |-------|-------------|----------------|-------------|----------------|-----------------|-------| | `Sitting` | **No** | No | No | No | Yes (head only) | Must StandUp to move | | `Injured` | **Slowed** (×0.65) | No | No | Yes | Yes | Limp anim. Heals above threshold | | `Hiding` (Locker/Under) | **No** | No | No | No | No (fixed cam) | Fully enclosed — zero movement | | `Hiding` (BehindCover) | **No** | No | No | N/A | Limited (peek) | Cannot leave cover position | | `Hiding` (InShadow) | **Yes** (slow) | No | No | Yes | Yes | Standing in shadow | | `Hiding` (TallGrass) | **Yes** (crouch-walk) | No | No | Yes | Yes | Crouch-moving through vegetation | | `Staggered` | **No** | No | No | No | Locked | Forced animation — no input | | `KnockedDown` | **No** | No | No | No | Locked (ground view) | Must GetUp animation first | | `Exhausted` | **No** | No | No | No | Yes | Heavy breathing — wait for regen | | `Panicked` | **Stumble** | No | No | No | Random jerks | Camera shakes | | `HoldingBreath` | **No** | No | No | No | Yes | Must release breath before moving | | `InPhotoMode` | **No** (free cam) | No | No | No | Full (free cam) | Gameplay input disabled | --- ## 9. GASP Integration Notes ### What BPC_StateManager READS from GASP | GASP Variable / Event | How Used | |----------------------|----------| | `ABP_GASP.bStrafing` | Read to detect if GASP is in strafing mode — informs combat state eligibility | | `ABP_GASP.bSprinting` | Read to confirm sprint state when `BPC_MovementStateSystem` reports sprint | | `ABP_GASP.MovementMode` | Read to cross-validate `BPC_MovementStateSystem.CurrentMovementMode` | | `ABP_GASP.GroundState` | Read to know if airborne (blocks certain actions) | | `AnimNotify_StateEnter` | Any GASP notify received via `I_AnimNotifyInterface` is forwarded to listeners | ### What BPC_StateManager WRITES to GASP | GASP Variable / Event | How Written | |----------------------|-------------| | `ABP_GASP.OverlayState` | Set via `SetOverlayState(E_OverlayState)` — direct enum value set on cached AnimBP reference | | `ABP_GASP.bIsInAction` | Set to `true` when `CurrentState` is any action state (not Idle/Walking/Jogging/Sprinting/Crouching) | | `ABP_GASP.ActionIntensity` | Float 0.0–1.0 set based on action type: 0.0=Idle, 0.3=Interacting, 0.7=SwingMelee, 1.0=FiringFirearm | | `AnimNotify_OverlayChanged` | Fired when overlay changes — GASP blends to new upper-body pose | ### What BPC_StateManager NEVER Touches - **Motion Matching Database** — GASP's motion matching is sacred, never modified - **Camera Manager** — Camera blends, FOV, and offsets belong to `BPC_CameraStateLayer` - **GASP's Internal State Machine** — Ground/air transitions, rotation mode, strafing logic - **Animation Blueprint Graph Logic** — Only sets variables, never modifies graph nodes - **GASP's Root Motion** — Root motion extraction is GASP's domain --- ## 10. Communication Matrix | Who Talks | How | What Is Sent | |-----------|-----|-------------| | Any System | `RequestStateChange()` | `E_PlayerActionState`, `RequesterTag: GameplayTag`, `Priority: Int` | | Any System | `IsActionPermitted()` | `GameplayTag` → `S_ActionPermissionResult` | | BPC_StateManager | `ABP_GASP` (Direct variable set) | `OverlayState`, `bIsInAction`, `ActionIntensity` | | BPC_StateManager | `Dispatcher: OnStateChanged` | `OldState`, `NewState` to all bound listeners | | BPC_StateManager | `Dispatcher: OnOverlayStateChanged` | `OldOverlay`, `NewOverlay` to `BPC_EmbodimentSystem`, `WBP_HUDController` | | BPC_StateManager | `Dispatcher: OnStateChangeDenied` | Request, Result, Reason to `WBP_InteractionPromptDisplay`, `WBP_NotificationToast` | | BPC_StateManager | `SS_EnhancedInputManager` (Direct) | Context change notification when state changes | | BPC_StateManager | `BPC_MovementStateSystem` (Direct) | Receives `OnMovementModeChanged` to track walking/sprinting | | BPC_StateManager | `BPC_HealthSystem` (Listener on OnDeath) | Auto-transitions to Dead state | | BPC_StateManager | `Dispatcher: OnVitalSignalChanged` | `OldSignal`, `NewSignal` to `SS_AudioManager`, `BPC_CameraStateLayer` | | BPC_StateManager | `Dispatcher: OnHeartRateChanged` | `NewBPM: Float` to `SS_AudioManager`, `WBP_AccessibilityUI` | | `GI_GameFramework` | `Dispatcher: OnGamePhaseChanged` | Notifies StateManager of phase changes for gating | | `BPC_CameraStateLayer` | `Dispatcher: OnStateChanged` | Adjusts camera based on state (e.g., combat FOV) | | `BPC_StressSystem` | `Direct: IsActionPermitted` | Checks if stress effects are currently permitted | | `WBP_InteractionPromptDisplay` | `Listener on OnStateChanged` | Shows/hides interaction prompts based on state | --- ## 11. Validation / Testing Checklist - [ ] `RequestStateChange` with valid state returns `Permitted` and fires `OnStateChanged` - [ ] `RequestStateChange` while Dead (except to Idle) returns `Blocked_Dead` - [ ] `RequestStateChange` during transition queues and returns `Blocked_CurrentState` - [ ] `ForceStateChange` bypasses all gating and pushes to ForceStack - [ ] `RestorePreviousState` pops ForceStack and restores correct state - [ ] `IsActionPermitted` returns correct `S_ActionPermissionResult` for all 35+ actions - [ ] `SetOverlayState` correctly writes to `ABP_GASP.OverlayState` - [ ] Overlay change fires `OnOverlayStateChanged` dispatcher - [ ] Game phase change (MainMenu → InGame → Cutscene) correctly gates actions - [ ] Death auto-transitions to Dead state via `BPC_HealthSystem.OnDeath` binding - [ ] Respawn restores previous state from ForceStack - [ ] Rapid state change requests are throttled by `StateTransitionDelay` - [ ] `IsInCombat()` returns true for `SwingMelee`, `AimingFirearm`, `FiringFirearm`, `Reloading`, `DeployingShield` - [ ] `IsMovementBlocked()` returns true for all blocking states - [ ] Heart rate correctly interpolates and fires threshold change events - [ ] Vital signal changes fire `OnVitalSignalChanged` - [ ] Injury state auto-transitions when health crosses threshold - [ ] Edge case: `ForceStateChange` called twice with same state — second call is no-op - [ ] Edge case: `RestorePreviousState` with empty ForceStack defaults to Idle - [ ] Edge case: State Manager handles owner destruction gracefully (null checks on cached ABP ref) - [ ] Edge case: `RequestStateChange` with invalid/missing RequesterTag logs warning --- ## 12. Reuse Notes - BPC_StateManager replaces the need for every system to check every other system's state. Systems call `IsActionPermitted(Tag)` instead of `IsPlayerInState(X) && !IsPlayerInState(Y) && ...` - New states can be added to `E_PlayerActionState` and `DA_StateGatingTable` without modifying the State Manager blueprint logic. - The `DA_StateGatingTable` Data Asset allows designers to tune gating rules without opening blueprints. - Force stack pattern supports nested force overrides (e.g., Death → Cutscene flashback → Respawn). - GASP variables `bIsInAction` and `ActionIntensity` allow AnimBP to blend differently during action vs exploration without the AnimBP knowing about game logic. - The action flag system (`RegisterActionFlag`/`UnregisterActionFlag`/`HasActionFlag`) enables lightweight transient permission tracking without formal state transitions. - `E_PlayerVitalSignals` drives audio heartbeat tempo, camera breathing sway, controller haptics, and accessibility visual heartbeat all from a single source of truth. --- *Blueprint Spec: BPC_StateManager — Central Nervous System for the Modular Game Framework. Architecture document: [`bpc-statemanager.md`](../../architecture/bpc-statemanager.md)*