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>
This commit is contained in:
j.foucher 2026-03-30 07:47:11 +02:00
parent 25abd59512
commit 98f0dbdce5
8 changed files with 257 additions and 15 deletions

View File

@ -178,6 +178,17 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::AtCover)); 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"), UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: at cover, ducking for %.1fs"),
*AIC->GetName(), Memory->PhaseDuration); *AIC->GetName(), Memory->PhaseDuration);
} }
@ -240,13 +251,12 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
} }
else else
{ {
// No better cover → stay, retry next cycle // No better cover found and no LOS → abandon cover, fall back to Attack task
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover; UE_LOG(LogPS_AI_Behavior, Log,
Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax); TEXT("[%s] CoverShootCycle: no LOS and no advancing cover → abandoning cover for Attack fallback"),
Memory->Timer = Memory->PhaseDuration; *AIC->GetName());
Memory->CycleCount = 0; FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, return;
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::AtCover));
} }
break; break;
} }
@ -255,13 +265,15 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
Memory->SubState = EPS_AI_Behavior_CombatSubState::Peeking; Memory->SubState = EPS_AI_Behavior_CombatSubState::Peeking;
Memory->PhaseDuration = FMath::RandRange(Memory->EffPeekMin, Memory->EffPeekMax); Memory->PhaseDuration = FMath::RandRange(Memory->EffPeekMin, Memory->EffPeekMax);
Memory->Timer = Memory->PhaseDuration; Memory->Timer = Memory->PhaseDuration;
Memory->LOSCheckTimer = 0.3f;
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::Peeking)); static_cast<uint8>(EPS_AI_Behavior_CombatSubState::Peeking));
// Start attacking // Stand up to shoot
if (Pawn->Implements<UPS_AI_Behavior_Interface>()) if (Pawn->Implements<UPS_AI_Behavior_Interface>())
{ {
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, false);
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target); IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
} }
@ -274,6 +286,40 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
// ─── PEEKING: Shooting at target ──────────────────────────────── // ─── PEEKING: Shooting at target ────────────────────────────────
case EPS_AI_Behavior_CombatSubState::Peeking: 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; Memory->Timer -= DeltaSeconds;
if (Memory->Timer <= 0.0f) if (Memory->Timer <= 0.0f)
{ {
@ -353,6 +399,17 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::AtCover)); 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, UE_LOG(LogPS_AI_Behavior, Verbose,
TEXT("[%s] CoverShootCycle: ducking back (cycle %d/%d)"), TEXT("[%s] CoverShootCycle: ducking back (cycle %d/%d)"),
*AIC->GetName(), Memory->CycleCount, Memory->EffMaxCycles); *AIC->GetName(), Memory->CycleCount, Memory->EffMaxCycles);
@ -375,6 +432,17 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::AtCover)); 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"), UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: arrived at new cover, ducking"),
*AIC->GetName()); *AIC->GetName());
} }
@ -391,11 +459,12 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::AbortTask(
{ {
AIC->StopMovement(); AIC->StopMovement();
// Stop attacking if we were peeking // Stop attacking and stand up if we were peeking/crouching
APawn* Pawn = AIC->GetPawn(); APawn* Pawn = AIC->GetPawn();
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>()) if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
{ {
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn); 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()); APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
if (AIC) if (AIC)
{ {
// Stop attacking // Stop attacking and stand up
APawn* Pawn = AIC->GetPawn(); APawn* Pawn = AIC->GetPawn();
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>()) if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
{ {
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn); IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, false);
} }
// Release cover point // Release cover point

View File

@ -2,6 +2,7 @@
#include "BT/PS_AI_Behavior_BTTask_FindCover.h" #include "BT/PS_AI_Behavior_BTTask_FindCover.h"
#include "PS_AI_Behavior_AIController.h" #include "PS_AI_Behavior_AIController.h"
#include "PS_AI_Behavior_Interface.h"
#include "PS_AI_Behavior_CoverPoint.h" #include "PS_AI_Behavior_CoverPoint.h"
#include "PS_AI_Behavior_PersonalityComponent.h" #include "PS_AI_Behavior_PersonalityComponent.h"
#include "PS_AI_Behavior_Definitions.h" #include "PS_AI_Behavior_Definitions.h"
@ -11,6 +12,7 @@
#include "CollisionQueryParams.h" #include "CollisionQueryParams.h"
#include "Engine/World.h" #include "Engine/World.h"
#include "EngineUtils.h" #include "EngineUtils.h"
#include "EnvironmentQuery/EnvQueryManager.h"
UPS_AI_Behavior_BTTask_FindCover::UPS_AI_Behavior_BTTask_FindCover() 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); 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( const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
BestCoverPos, AcceptanceRadius, /*bStopOnOverlap=*/true, BestCoverPos, AcceptanceRadius, /*bStopOnOverlap=*/true,
/*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true, /*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true,
@ -136,7 +147,6 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindCover::ExecuteTask(
return EBTNodeResult::Succeeded; return EBTNodeResult::Succeeded;
} }
FCoverMemory* Memory = reinterpret_cast<FCoverMemory*>(NodeMemory);
Memory->bMoveRequested = true; Memory->bMoveRequested = true;
return EBTNodeResult::InProgress; return EBTNodeResult::InProgress;
} }
@ -145,6 +155,7 @@ void UPS_AI_Behavior_BTTask_FindCover::TickTask(
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{ {
FCoverMemory* Memory = reinterpret_cast<FCoverMemory*>(NodeMemory); FCoverMemory* Memory = reinterpret_cast<FCoverMemory*>(NodeMemory);
if (Memory->bEQSRunning) return; // Waiting for EQS callback
if (!Memory->bMoveRequested) return; if (!Memory->bMoveRequested) return;
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner()); 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) if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
{ {
Memory->bMoveRequested = false; 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); FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
} }
} }
@ -165,6 +193,13 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindCover::AbortTask(
{ {
AIC->StopMovement(); 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 // Release any claimed cover point
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent(); UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
if (BB) 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)); Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
if (Point) if (Point)
{ {
Point->Release(AIC->GetPawn()); if (Pawn) Point->Release(Pawn);
BB->ClearValue(PS_AI_Behavior_BB::CoverPoint); 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 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, SearchRadius,
CoverPointType == EPS_AI_Behavior_CoverPointType::Cover ? TEXT("Cover") : TEXT("Hiding"), CoverPointType == EPS_AI_Behavior_CoverPointType::Cover ? TEXT("Cover") : TEXT("Hiding"),
NumCandidates, 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

@ -87,6 +87,7 @@ private:
float PhaseDuration = 0.0f; float PhaseDuration = 0.0f;
int32 CycleCount = 0; int32 CycleCount = 0;
bool bMoveRequested = false; bool bMoveRequested = false;
float LOSCheckTimer = 0.0f; // cooldown for LOS checks during Peeking
// Effective durations (modulated by personality) // Effective durations (modulated by personality)
float EffPeekMin = 2.0f; float EffPeekMin = 2.0f;

View File

@ -4,10 +4,12 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h" #include "BehaviorTree/BTTaskNode.h"
#include "EnvironmentQuery/EnvQueryTypes.h"
#include "PS_AI_Behavior_Definitions.h" #include "PS_AI_Behavior_Definitions.h"
#include "PS_AI_Behavior_BTTask_FindCover.generated.h" #include "PS_AI_Behavior_BTTask_FindCover.generated.h"
class APS_AI_Behavior_CoverPoint; class APS_AI_Behavior_CoverPoint;
class UEnvQuery;
/** /**
* BT Task: Find a cover position and navigate to it. * 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")) UPROPERTY(EditAnywhere, Category = "Cover|Advancement", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float AdvancementBias = 0.0f; 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: protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override; virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
@ -80,6 +95,7 @@ private:
struct FCoverMemory struct FCoverMemory
{ {
bool bMoveRequested = false; bool bMoveRequested = false;
bool bEQSRunning = false;
}; };
virtual uint16 GetInstanceMemorySize() const override { return sizeof(FCoverMemory); } virtual uint16 GetInstanceMemorySize() const override { return sizeof(FCoverMemory); }
@ -98,4 +114,13 @@ private:
APS_AI_Behavior_CoverPoint* FindBestManualCoverPoint( APS_AI_Behavior_CoverPoint* FindBestManualCoverPoint(
const UWorld* World, const FVector& NpcLoc, const FVector& ThreatLoc, const UWorld* World, const FVector& NpcLoc, const FVector& ThreatLoc,
EPS_AI_Behavior_NPCType NPCType, float& OutScore) const; 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") UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
bool CanBehaviorAttack(AActor* Target) const; 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 ─────────────────────────────────────────────────── // ─── Combat Style ───────────────────────────────────────────────────
/** /**