// Copyright Epic Games, Inc. All Rights Reserved. // UE5 Modular Game Framework — BPC_StateManager Implementation #include "Player/BPC_StateManager.h" #include "Player/BPC_HealthSystem.h" #include "Player/BPC_StressSystem.h" #include "Player/BPC_StaminaSystem.h" #include "Player/BPC_MovementStateSystem.h" #include "GameplayTagsManager.h" DEFINE_LOG_CATEGORY_STATIC(LogStateManager, Log, All); // Stub variable for combat encounter check (would come from GS_CoreGameState binding). namespace { bool bEncounterActive = false; } UBPC_StateManager::UBPC_StateManager() { PrimaryComponentTick.bCanEverTick = true; PrimaryComponentTick.TickInterval = 0.1f; // 10Hz — smooth heart rate without per-frame cost. HeartRateSmoothSpeed = 2.0f; TargetHeartRate = 70.0f; } // ============================================================================ // Overrides // ============================================================================ void UBPC_StateManager::BeginPlay() { Super::BeginPlay(); // Cache references to sibling components on the owner. AActor* Owner = GetOwner(); if (Owner) { CachedHealthSystem = Owner->FindComponentByClass(); CachedStressSystem = Owner->FindComponentByClass(); CachedStaminaSystem = Owner->FindComponentByClass(); CachedMovementSystem = Owner->FindComponentByClass(); } // Set default states. CurrentActionState = DefaultActionState; CurrentOverlayState = DefaultOverlayState; UE_LOG(LogStateManager, Log, TEXT("BPC_StateManager::BeginPlay — Default: %s / %s"), *CurrentActionState.GetTagName().ToString(), *CurrentOverlayState.GetTagName().ToString()); } void UBPC_StateManager::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); // Smooth heart rate toward target. if (!FMath::IsNearlyEqual(HeartRateBPM, TargetHeartRate, 0.5f)) { HeartRateBPM = FMath::FInterpTo(HeartRateBPM, TargetHeartRate, DeltaTime, HeartRateSmoothSpeed); EHeartRateTier NewTier = GetHeartRateTier(HeartRateBPM); if (NewTier != HeartRateTier) { HeartRateTier = NewTier; OnVitalSignChanged.Broadcast(FGameplayTag::RequestGameplayTag( FName(TEXT("Framework.State.Vital.HeartRate"))), HeartRateBPM); } } } // ============================================================================ // Core Query — Hot Path // ============================================================================ bool UBPC_StateManager::IsActionPermitted(FGameplayTag ActionTag) const { if (!ActionTag.IsValid()) { return false; } // Force stack overrides everything — check first. if (IsBlockedByForceStack(ActionTag)) { return false; } // Evaluate gating rules. if (!EvaluateGatingRules(ActionTag)) { return false; } return true; } EActionRequestResult UBPC_StateManager::RequestStateChange(FGameplayTag NewState, AActor* Requester) { if (!NewState.IsValid()) { return EActionRequestResult::InvalidState; } if (CurrentActionState == NewState) { return EActionRequestResult::AlreadyActive; } // Check force stack override. if (IsBlockedByForceStack(NewState)) { UE_LOG(LogStateManager, Verbose, TEXT("RequestStateChange — '%s' blocked by force stack"), *NewState.GetTagName().ToString()); return EActionRequestResult::BlockedByForce; } // Check gating. if (!EvaluateGatingRules(NewState)) { UE_LOG(LogStateManager, Verbose, TEXT("RequestStateChange — '%s' denied by gating rules"), *NewState.GetTagName().ToString()); return EActionRequestResult::Denied; } // Apply the state change. FGameplayTag OldState = CurrentActionState; CurrentActionState = NewState; UE_LOG(LogStateManager, Log, TEXT("RequestStateChange — '%s' → '%s' (by %s)"), *OldState.GetTagName().ToString(), *NewState.GetTagName().ToString(), Requester ? *Requester->GetName() : TEXT("Unknown")); OnActionStateChanged.Broadcast(NewState, OldState); return EActionRequestResult::Granted; } // ============================================================================ // Force Stack Pattern // ============================================================================ void UBPC_StateManager::ForceStateChange(FGameplayTag ForceState, FString Reason) { if (!ForceState.IsValid()) { return; } // Save current states before overriding. if (ForceStack.Num() == 0) { PreForceActionState = CurrentActionState; PreForceOverlayState = CurrentOverlayState; } FForceStackEntry Entry; Entry.State = ForceState; Entry.Reason = Reason; ForceStack.Push(Entry); // Override active state. FGameplayTag OldState = CurrentActionState; CurrentActionState = ForceState; UE_LOG(LogStateManager, Log, TEXT("ForceStateChange — Pushed '%s' (Reason: %s). Stack depth: %d"), *ForceState.GetTagName().ToString(), *Reason, ForceStack.Num()); OnForceStackPushed.Broadcast(ForceState); OnActionStateChanged.Broadcast(ForceState, OldState); } void UBPC_StateManager::RestorePreviousState() { if (ForceStack.Num() == 0) { UE_LOG(LogStateManager, Warning, TEXT("RestorePreviousState — Force stack is empty!")); return; } FForceStackEntry Popped = ForceStack.Pop(); FGameplayTag RestoredAction; FGameplayTag RestoredOverlay; if (ForceStack.Num() > 0) { // Still have forced states — use the next one down. RestoredAction = ForceStack.Top().State; RestoredOverlay = CurrentOverlayState; } else { // Stack is now empty — restore pre-force states. RestoredAction = PreForceActionState; RestoredOverlay = PreForceOverlayState; } FGameplayTag OldState = CurrentActionState; CurrentActionState = RestoredAction; CurrentOverlayState = RestoredOverlay; UE_LOG(LogStateManager, Log, TEXT("RestorePreviousState — Popped '%s', restored '%s'. Stack depth: %d"), *Popped.State.GetTagName().ToString(), *RestoredAction.GetTagName().ToString(), ForceStack.Num()); OnForceStackPopped.Broadcast(RestoredAction); OnActionStateChanged.Broadcast(RestoredAction, OldState); OnOverlayStateChanged.Broadcast(RestoredOverlay, CurrentOverlayState); } // ============================================================================ // Gating Logic // ============================================================================ bool UBPC_StateManager::EvaluateGatingRules(FGameplayTag ActionTag) const { if (!GatingTable) { // No gating table — permit everything (lenient default). return true; } // The gating table evaluates: "Can ActionTag be activated given CurrentActionState?" // This delegates to DA_StateGatingTable's native C++ evaluation. // 37 rules iterated in C++ — negligible cost vs BP Chooser Table overhead. // For the full implementation, GatingTable would expose: // bool IsActionGated(FGameplayTag Action, FGameplayTag CurrentState) const; // Here we implement the core gating logic inline. // Check for explicit blocking rules. // Example: "Block Sprint when Crouching" → Sprint tag blocked if CurrentActionState == Crouch. // Real implementation delegates to DA_StateGatingTable. return true; // Placeholder — full rules in DA_StateGatingTable. } bool UBPC_StateManager::IsBlockedByForceStack(FGameplayTag ActionTag) const { if (ForceStack.Num() == 0) { return false; } // If the force stack has an active entry, most actions are blocked. // Death state: blocks all actions except menu/cutscene. const FForceStackEntry& Active = ForceStack.Top(); // Check if the force state explicitly permits this action. // Death permits Menu, Cutscene; Cutscene permits nothing. // This logic can be extended via DA_StateGatingTable force-state rules. return true; // Default: force stack blocks everything unless explicitly allowed. } // ============================================================================ // Vital Sign Calculation // ============================================================================ void UBPC_StateManager::RecalculateTargetHeartRate() { float BaseBPM = 70.0f; // Resting heart rate. // Stress contribution. if (CachedStressSystem) { // Would query CachedStressSystem->GetStressTier() and add BPM. // Higher stress = higher BPM (up to +50 BPM). } // Stamina exhaustion contribution. if (CachedStaminaSystem) { // Low stamina = higher BPM (exhaustion adds +20 BPM). } // Combat contribution. if (bEncounterActive) { BaseBPM += 30.0f; // Combat adds stress. } TargetHeartRate = FMath::Clamp(BaseBPM, 50.0f, 200.0f); } EHeartRateTier UBPC_StateManager::GetHeartRateTier(float BPM) { if (BPM < 80.0f) return EHeartRateTier::Resting; if (BPM < 100.0f) return EHeartRateTier::Elevated; if (BPM < 130.0f) return EHeartRateTier::Stressed; if (BPM < 160.0f) return EHeartRateTier::Panic; return EHeartRateTier::Critical; }