diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.dll b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.dll index 48fc711..217d11c 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.dll and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.dll differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.exp index 75393b4..469e727 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.exp and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.exp differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.pdb index 288e195..c7b13de 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.pdb and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.pdb differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_CoverShootCycle.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_CoverShootCycle.cpp index 5bfb6d9..260b869 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_CoverShootCycle.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_CoverShootCycle.cpp @@ -178,6 +178,17 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask( BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, static_cast(EPS_AI_Behavior_CombatSubState::AtCover)); + // Crouch if the cover point requires it + if (Pawn->Implements()) + { + const APS_AI_Behavior_CoverPoint* CoverPt = + Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); + if (CoverPt && CoverPt->bCrouch) + { + IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); + } + } + UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: at cover, ducking for %.1fs"), *AIC->GetName(), Memory->PhaseDuration); } @@ -240,13 +251,12 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask( } else { - // No better cover → stay, retry next cycle - Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover; - Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax); - Memory->Timer = Memory->PhaseDuration; - Memory->CycleCount = 0; - BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, - static_cast(EPS_AI_Behavior_CombatSubState::AtCover)); + // No better cover found and no LOS → abandon cover, fall back to Attack task + UE_LOG(LogPS_AI_Behavior, Log, + TEXT("[%s] CoverShootCycle: no LOS and no advancing cover → abandoning cover for Attack fallback"), + *AIC->GetName()); + FinishLatentTask(OwnerComp, EBTNodeResult::Failed); + return; } break; } @@ -255,13 +265,15 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask( Memory->SubState = EPS_AI_Behavior_CombatSubState::Peeking; Memory->PhaseDuration = FMath::RandRange(Memory->EffPeekMin, Memory->EffPeekMax); Memory->Timer = Memory->PhaseDuration; + Memory->LOSCheckTimer = 0.3f; BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, static_cast(EPS_AI_Behavior_CombatSubState::Peeking)); - // Start attacking + // Stand up to shoot if (Pawn->Implements()) { + IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, false); IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target); } @@ -274,6 +286,40 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask( // ─── PEEKING: Shooting at target ──────────────────────────────── case EPS_AI_Behavior_CombatSubState::Peeking: { + // Continuous LOS check while peeking — stop shooting if target hides + Memory->LOSCheckTimer -= DeltaSeconds; + if (Memory->LOSCheckTimer <= 0.0f) + { + Memory->LOSCheckTimer = 0.3f; // check every 0.3s + const bool bStillHasLOS = UPS_AI_Behavior_Statics::HasLineOfSight( + Pawn->GetWorld(), Pawn, Target, 150.0f); + if (!bStillHasLOS) + { + // Target hid — stop shooting, crouch back to cover + if (Pawn->Implements()) + { + IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn); + const APS_AI_Behavior_CoverPoint* CoverPt = + Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); + if (CoverPt && CoverPt->bCrouch) + { + IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); + } + } + + UE_LOG(LogPS_AI_Behavior, Log, + TEXT("[%s] CoverShootCycle: lost LOS during peek → back to cover"), + *AIC->GetName()); + + Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover; + Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax); + Memory->Timer = Memory->PhaseDuration; + BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, + static_cast(EPS_AI_Behavior_CombatSubState::AtCover)); + break; + } + } + Memory->Timer -= DeltaSeconds; if (Memory->Timer <= 0.0f) { @@ -353,6 +399,17 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask( BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, static_cast(EPS_AI_Behavior_CombatSubState::AtCover)); + // Re-crouch if cover requires it + if (Pawn->Implements()) + { + const APS_AI_Behavior_CoverPoint* CoverPt = + Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); + if (CoverPt && CoverPt->bCrouch) + { + IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); + } + } + UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: ducking back (cycle %d/%d)"), *AIC->GetName(), Memory->CycleCount, Memory->EffMaxCycles); @@ -375,6 +432,17 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask( BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, static_cast(EPS_AI_Behavior_CombatSubState::AtCover)); + // Crouch if the new cover point requires it + if (Pawn->Implements()) + { + const APS_AI_Behavior_CoverPoint* CoverPt = + Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); + if (CoverPt && CoverPt->bCrouch) + { + IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); + } + } + UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: arrived at new cover, ducking"), *AIC->GetName()); } @@ -391,11 +459,12 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::AbortTask( { AIC->StopMovement(); - // Stop attacking if we were peeking + // Stop attacking and stand up if we were peeking/crouching APawn* Pawn = AIC->GetPawn(); if (Pawn && Pawn->Implements()) { IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn); + IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, false); } } @@ -408,11 +477,12 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::OnTaskFinished( APS_AI_Behavior_AIController* AIC = Cast(OwnerComp.GetAIOwner()); if (AIC) { - // Stop attacking + // Stop attacking and stand up APawn* Pawn = AIC->GetPawn(); if (Pawn && Pawn->Implements()) { IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn); + IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, false); } // Release cover point diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindCover.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindCover.cpp index 2f82793..c43c91d 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindCover.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindCover.cpp @@ -2,6 +2,7 @@ #include "BT/PS_AI_Behavior_BTTask_FindCover.h" #include "PS_AI_Behavior_AIController.h" +#include "PS_AI_Behavior_Interface.h" #include "PS_AI_Behavior_CoverPoint.h" #include "PS_AI_Behavior_PersonalityComponent.h" #include "PS_AI_Behavior_Definitions.h" @@ -11,6 +12,7 @@ #include "CollisionQueryParams.h" #include "Engine/World.h" #include "EngineUtils.h" +#include "EnvironmentQuery/EnvQueryManager.h" UPS_AI_Behavior_BTTask_FindCover::UPS_AI_Behavior_BTTask_FindCover() { @@ -120,7 +122,16 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindCover::ExecuteTask( BB->SetValueAsVector(PS_AI_Behavior_BB::CoverLocation, BestCoverPos); - // Navigate to cover + FCoverMemory* Memory = reinterpret_cast(NodeMemory); + + // If we have a refinement EQS query and a manual CoverPoint, refine the position + if (RefinementQuery && ChosenPoint) + { + RunRefinementQuery(OwnerComp, NodeMemory, AIC->GetPawn(), BestCoverPos); + return EBTNodeResult::InProgress; + } + + // Navigate to cover directly (no refinement) const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation( BestCoverPos, AcceptanceRadius, /*bStopOnOverlap=*/true, /*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true, @@ -136,7 +147,6 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindCover::ExecuteTask( return EBTNodeResult::Succeeded; } - FCoverMemory* Memory = reinterpret_cast(NodeMemory); Memory->bMoveRequested = true; return EBTNodeResult::InProgress; } @@ -145,6 +155,7 @@ void UPS_AI_Behavior_BTTask_FindCover::TickTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) { FCoverMemory* Memory = reinterpret_cast(NodeMemory); + if (Memory->bEQSRunning) return; // Waiting for EQS callback if (!Memory->bMoveRequested) return; APS_AI_Behavior_AIController* AIC = Cast(OwnerComp.GetAIOwner()); @@ -153,6 +164,23 @@ void UPS_AI_Behavior_BTTask_FindCover::TickTask( if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle) { Memory->bMoveRequested = false; + + // Crouch at cover if the point requires it + APawn* Pawn = AIC->GetPawn(); + if (Pawn && Pawn->Implements()) + { + UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent(); + if (BB) + { + const APS_AI_Behavior_CoverPoint* CoverPt = + Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); + if (CoverPt && CoverPt->bCrouch) + { + IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); + } + } + } + FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded); } } @@ -165,6 +193,13 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindCover::AbortTask( { AIC->StopMovement(); + // Stand up if crouching + APawn* Pawn = AIC->GetPawn(); + if (Pawn && Pawn->Implements()) + { + IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, false); + } + // Release any claimed cover point UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent(); if (BB) @@ -173,7 +208,7 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindCover::AbortTask( Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); if (Point) { - Point->Release(AIC->GetPawn()); + if (Pawn) Point->Release(Pawn); BB->ClearValue(PS_AI_Behavior_BB::CoverPoint); } } @@ -286,9 +321,108 @@ float UPS_AI_Behavior_BTTask_FindCover::EvaluateCoverQuality( FString UPS_AI_Behavior_BTTask_FindCover::GetStaticDescription() const { - return FString::Printf(TEXT("Find cover within %.0fcm\nManual %s + Procedural (%d candidates)\nBonus: +%.0f%%"), + return FString::Printf(TEXT("Find cover within %.0fcm\nManual %s + Procedural (%d candidates)\nBonus: +%.0f%%\nRefinement: %s"), SearchRadius, CoverPointType == EPS_AI_Behavior_CoverPointType::Cover ? TEXT("Cover") : TEXT("Hiding"), NumCandidates, - ManualPointBonus * 100.0f); + ManualPointBonus * 100.0f, + RefinementQuery ? *RefinementQuery->GetName() : TEXT("None")); +} + +// ─── EQS Refinement ──────────────────────────────────────────────────────── + +void UPS_AI_Behavior_BTTask_FindCover::RunRefinementQuery( + UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, + APawn* Pawn, const FVector& CoverCenter) +{ + FCoverMemory* Memory = reinterpret_cast(NodeMemory); + + UWorld* World = Pawn->GetWorld(); + UEnvQueryManager* EQSManager = UEnvQueryManager::GetCurrent(World); + if (!EQSManager) + { + // Fallback: move directly to the cover center + APS_AI_Behavior_AIController* AIC = Cast(OwnerComp.GetAIOwner()); + if (AIC) + { + AIC->MoveToLocation(CoverCenter, AcceptanceRadius, true, true, true, false); + Memory->bMoveRequested = true; + } + return; + } + + Memory->bEQSRunning = true; + + FEnvQueryRequest Request(RefinementQuery, Pawn); + Request.Execute(EEnvQueryRunMode::SingleResult, + FQueryFinishedSignature::CreateUObject(this, + &UPS_AI_Behavior_BTTask_FindCover::OnRefinementQueryFinished, + &OwnerComp, NodeMemory, CoverCenter)); + + UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] FindCover: EQS refinement query launched around %s"), + *Pawn->GetName(), *CoverCenter.ToString()); +} + +void UPS_AI_Behavior_BTTask_FindCover::OnRefinementQueryFinished( + TSharedPtr Result, + UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory, + FVector OriginalCoverPos) +{ + if (!OwnerComp || !NodeMemory) return; + + FCoverMemory* Memory = reinterpret_cast(NodeMemory); + Memory->bEQSRunning = false; + + APS_AI_Behavior_AIController* AIC = Cast(OwnerComp->GetAIOwner()); + if (!AIC || !AIC->GetPawn()) return; + + FVector FinalPos = OriginalCoverPos; // Fallback to original position + + if (Result.IsValid() && Result->IsSuccessful()) + { + FinalPos = Result->GetItemAsLocation(0); + UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] FindCover: EQS refined position %s (was %s)"), + *AIC->GetName(), *FinalPos.ToString(), *OriginalCoverPos.ToString()); + } + else + { + UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] FindCover: EQS refinement failed, using original cover position"), + *AIC->GetName()); + } + + // Update BB with refined position + UBlackboardComponent* BB = OwnerComp->GetBlackboardComponent(); + if (BB) + { + BB->SetValueAsVector(PS_AI_Behavior_BB::CoverLocation, FinalPos); + } + + // Navigate to the refined (or original) position + const EPathFollowingRequestResult::Type MoveResult = AIC->MoveToLocation( + FinalPos, AcceptanceRadius, true, true, true, false); + + if (MoveResult == EPathFollowingRequestResult::Failed) + { + FinishLatentTask(*OwnerComp, EBTNodeResult::Failed); + return; + } + + if (MoveResult == EPathFollowingRequestResult::AlreadyAtGoal) + { + // Crouch at cover if needed + APawn* Pawn = AIC->GetPawn(); + if (Pawn && Pawn->Implements()) + { + const APS_AI_Behavior_CoverPoint* CoverPt = + Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); + if (CoverPt && CoverPt->bCrouch) + { + IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); + } + } + FinishLatentTask(*OwnerComp, EBTNodeResult::Succeeded); + return; + } + + Memory->bMoveRequested = true; } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_CoverShootCycle.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_CoverShootCycle.h index eb64fd4..b4397b6 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_CoverShootCycle.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_CoverShootCycle.h @@ -87,6 +87,7 @@ private: float PhaseDuration = 0.0f; int32 CycleCount = 0; bool bMoveRequested = false; + float LOSCheckTimer = 0.0f; // cooldown for LOS checks during Peeking // Effective durations (modulated by personality) float EffPeekMin = 2.0f; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_FindCover.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_FindCover.h index 78ed654..b2f094b 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_FindCover.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_FindCover.h @@ -4,10 +4,12 @@ #include "CoreMinimal.h" #include "BehaviorTree/BTTaskNode.h" +#include "EnvironmentQuery/EnvQueryTypes.h" #include "PS_AI_Behavior_Definitions.h" #include "PS_AI_Behavior_BTTask_FindCover.generated.h" class APS_AI_Behavior_CoverPoint; +class UEnvQuery; /** * BT Task: Find a cover position and navigate to it. @@ -70,6 +72,19 @@ public: UPROPERTY(EditAnywhere, Category = "Cover|Advancement", meta = (ClampMin = "0.0", ClampMax = "1.0")) float AdvancementBias = 0.0f; + /** + * Optional EQS query to refine the exact cover position around a selected CoverPoint. + * The query runs centered on the chosen CoverPoint and picks the best nearby spot. + * Use OnCircle generator (small radius ~200cm) + CoverQuality test + Distance tests. + * If null, the NPC goes directly to the CoverPoint's location (no refinement). + */ + UPROPERTY(EditAnywhere, Category = "Cover|EQS Refinement") + TObjectPtr RefinementQuery; + + /** Radius around the CoverPoint for EQS refinement search (cm). */ + UPROPERTY(EditAnywhere, Category = "Cover|EQS Refinement", meta = (ClampMin = "100.0", ClampMax = "500.0")) + float RefinementRadius = 200.0f; + protected: virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override; @@ -80,6 +95,7 @@ private: struct FCoverMemory { bool bMoveRequested = false; + bool bEQSRunning = false; }; virtual uint16 GetInstanceMemorySize() const override { return sizeof(FCoverMemory); } @@ -98,4 +114,13 @@ private: APS_AI_Behavior_CoverPoint* FindBestManualCoverPoint( const UWorld* World, const FVector& NpcLoc, const FVector& ThreatLoc, EPS_AI_Behavior_NPCType NPCType, float& OutScore) const; + + /** Run EQS refinement query around the chosen CoverPoint. */ + void RunRefinementQuery(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, + APawn* Pawn, const FVector& CoverCenter); + + /** Callback when the refinement EQS query completes. */ + void OnRefinementQueryFinished(TSharedPtr Result, + UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory, + FVector OriginalCoverPos); }; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h index dc6838a..6aee683 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h @@ -142,6 +142,18 @@ public: UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior") bool CanBehaviorAttack(AActor* Target) const; + // ─── Stance ───────────────────────────────────────────────────────── + + /** + * Order the Pawn to crouch or stand up. + * Called by the cover system when entering/leaving a cover point with bCrouch set. + * The Pawn implements this however it wants (CharacterMovement->Crouch, animation, etc.). + * + * @param bCrouch True = crouch, False = stand up. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior") + void SetBehaviorCrouch(bool bCrouch); + // ─── Combat Style ─────────────────────────────────────────────────── /**