Compare commits

..

2 Commits

Author SHA1 Message Date
98f0dbdce5 Add cover system LOS checks, crouch interface, and EQS refinement
- SetBehaviorCrouch() interface function for cover/hiding crouch control
- CoverShootCycle: continuous LOS check during Peeking (stop if target hides)
- CoverShootCycle: crouch/stand transitions at all cover state changes
- CoverShootCycle: fail when no LOS and no advancing cover (falls to Attack)
- FindCover: crouch on arrival, stand up on abort
- FindCover: optional EQS RefinementQuery to refine exact position around CoverPoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 07:47:11 +02:00
25abd59512 Fix EQS firing position: invalid BB vector check, AlreadyAtGoal fallback
- LastKnownTargetPosition check now filters FLT_MAX/InvalidLocation (not just zero)
- EQS result with AlreadyAtGoal triggers fallback advance instead of no-op
- bProjectGoal enabled for EQS move to handle NavMesh projection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 11:14:04 +02:00
14 changed files with 291 additions and 31 deletions

View File

@ -227,8 +227,12 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask(
{
// No LOS and idle → find a firing position
// Check if investigation (last known position) is active
// ClearValue sets vectors to FAISystem::InvalidLocation, not zero — must check both
const FVector LastKnownPos = BB->GetValueAsVector(PS_AI_Behavior_BB::LastKnownTargetPosition);
if (!LastKnownPos.IsZero())
const bool bHasLastKnown = !LastKnownPos.IsZero() &&
!LastKnownPos.ContainsNaN() &&
LastKnownPos.X < 1e30f; // Filter out FLT_MAX / InvalidLocation
if (bHasLastKnown)
{
// Investigation mode: go to last known position
AIC->MoveToLocation(
@ -336,6 +340,7 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask(
*AIC->GetName());
}
}
}
EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask(
@ -427,20 +432,9 @@ void UPS_AI_Behavior_BTTask_Attack::OnFiringPositionQueryFinished(
AActor* Target = WeakTarget.Get();
if (Result.IsValid() && Result->IsSuccessful())
// Fallback lambda: advance directly toward target
auto FallbackAdvance = [&](const TCHAR* Reason)
{
const FVector FiringPos = Result->GetItemAsLocation(0);
AIC->MoveToLocation(
FiringPos, 50.0f, /*bStopOnOverlap=*/true,
/*bUsePathfinding=*/true, /*bProjectGoal=*/false, /*bCanStrafe=*/false);
Memory->bSeekingFiringPos = true;
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Attack: EQS found firing position %s"),
*AIC->GetName(), *FiringPos.ToString());
}
else
{
// EQS found nothing — fallback: advance toward target
if (Target)
{
const float MidRange = (Memory->MinRange + Memory->MaxRange) * 0.5f;
@ -449,9 +443,32 @@ void UPS_AI_Behavior_BTTask_Attack::OnFiringPositionQueryFinished(
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
Memory->bSeekingFiringPos = true;
}
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Attack: %s, fallback advance toward target"),
*AIC->GetName(), Reason);
};
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Attack: EQS no result, fallback advance"),
*AIC->GetName());
if (Result.IsValid() && Result->IsSuccessful())
{
const FVector FiringPos = Result->GetItemAsLocation(0);
const EPathFollowingRequestResult::Type MoveResult = AIC->MoveToLocation(
FiringPos, 50.0f, /*bStopOnOverlap=*/true,
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
if (MoveResult == EPathFollowingRequestResult::RequestSuccessful)
{
Memory->bSeekingFiringPos = true;
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Attack: EQS moving to firing position %s"),
*AIC->GetName(), *FiringPos.ToString());
}
else
{
// AlreadyAtGoal or Failed — EQS position is too close or unreachable
FallbackAdvance(TEXT("EQS position unreachable or too close"));
}
}
else
{
FallbackAdvance(TEXT("EQS no result"));
}
}

View File

@ -178,6 +178,17 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::AtCover));
// Crouch if the cover point requires it
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
{
const APS_AI_Behavior_CoverPoint* CoverPt =
Cast<APS_AI_Behavior_CoverPoint>(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<uint8>(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<uint8>(EPS_AI_Behavior_CombatSubState::Peeking));
// Start attacking
// Stand up to shoot
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
{
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<UPS_AI_Behavior_Interface>())
{
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
const APS_AI_Behavior_CoverPoint* CoverPt =
Cast<APS_AI_Behavior_CoverPoint>(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<uint8>(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<uint8>(EPS_AI_Behavior_CombatSubState::AtCover));
// Re-crouch if cover requires it
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
{
const APS_AI_Behavior_CoverPoint* CoverPt =
Cast<APS_AI_Behavior_CoverPoint>(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<uint8>(EPS_AI_Behavior_CombatSubState::AtCover));
// Crouch if the new cover point requires it
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
{
const APS_AI_Behavior_CoverPoint* CoverPt =
Cast<APS_AI_Behavior_CoverPoint>(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<UPS_AI_Behavior_Interface>())
{
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<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
if (AIC)
{
// Stop attacking
// Stop attacking and stand up
APawn* Pawn = AIC->GetPawn();
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
{
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, false);
}
// Release cover point

View File

@ -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<FCoverMemory*>(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<FCoverMemory*>(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<FCoverMemory*>(NodeMemory);
if (Memory->bEQSRunning) return; // Waiting for EQS callback
if (!Memory->bMoveRequested) return;
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(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<UPS_AI_Behavior_Interface>())
{
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
if (BB)
{
const APS_AI_Behavior_CoverPoint* CoverPt =
Cast<APS_AI_Behavior_CoverPoint>(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<UPS_AI_Behavior_Interface>())
{
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<APS_AI_Behavior_CoverPoint>(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<FCoverMemory*>(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<APS_AI_Behavior_AIController>(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<FEnvQueryResult> Result,
UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory,
FVector OriginalCoverPos)
{
if (!OwnerComp || !NodeMemory) return;
FCoverMemory* Memory = reinterpret_cast<FCoverMemory*>(NodeMemory);
Memory->bEQSRunning = false;
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(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<UPS_AI_Behavior_Interface>())
{
const APS_AI_Behavior_CoverPoint* CoverPt =
Cast<APS_AI_Behavior_CoverPoint>(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;
}

View File

@ -48,6 +48,7 @@ public:
UPROPERTY(EditAnywhere, Category = "Attack|LOS", meta = (ClampMin = "0.1", ClampMax = "1.0"))
float LOSCheckInterval = 0.2f;
/**
* EQS query asset for finding a firing position with clear LOS.
* Compose in editor: OnCircle generator + LineOfSight filter + Distance tests.

View File

@ -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;

View File

@ -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<UEnvQuery> 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<FEnvQueryResult> Result,
UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory,
FVector OriginalCoverPos);
};

View File

@ -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 ───────────────────────────────────────────────────
/**