Files
UE5-Modular-Game-Framework/Source/Framework/Private/Player/BPC_StateManager.cpp

291 lines
8.5 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
// UE5 Modular Game Framework — BPC_StateManager Implementation
#include "Player/BPC_StateManager.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<UBPC_HealthSystem>();
CachedStressSystem = Owner->FindComponentByClass<UBPC_StressSystem>();
CachedStaminaSystem = Owner->FindComponentByClass<UBPC_StaminaSystem>();
CachedMovementSystem = Owner->FindComponentByClass<UBPC_MovementStateSystem>();
}
// 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;
}