WIP: Cover system firing position, IsCoverNeeded decorator, EQS contexts
- CoverShootCycle: add FiringPositionQuery EQS for peek/shoot positions (NPC moves between cover position and firing position with LOS) - Add BTDecorator_IsCoverNeeded: skip cover when target is Civilian - Add EQSContext_CoverLocation: provides BB CoverLocation to EQS generators - FindCover: add debug draw toggle and EQS refinement debug spheres - Definitions: add ECoverShootSubState and CoverPointType::HidingSpot NOTE: Has compilation errors to fix (signature mismatches in CoverShootCycle StartPeeking/ReturnToCover, missing forward-declare) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
98f0dbdce5
commit
9aaf16c655
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,50 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTDecorator_IsCoverNeeded.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
|
||||
UPS_AI_Behavior_BTDecorator_IsCoverNeeded::UPS_AI_Behavior_BTDecorator_IsCoverNeeded()
|
||||
{
|
||||
NodeName = TEXT("Is Cover Needed");
|
||||
|
||||
// Default: cover needed against Protector and Enemy, not Civilian
|
||||
DangerousTargetTypes.Add(EPS_AI_Behavior_NPCType::Protector);
|
||||
DangerousTargetTypes.Add(EPS_AI_Behavior_NPCType::Enemy);
|
||||
}
|
||||
|
||||
bool UPS_AI_Behavior_BTDecorator_IsCoverNeeded::CalculateRawConditionValue(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
|
||||
{
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB) return false;
|
||||
|
||||
AActor* ThreatActor = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||
if (!ThreatActor) return false;
|
||||
|
||||
// If the target doesn't implement the interface, assume dangerous (safe default)
|
||||
APawn* ThreatPawn = Cast<APawn>(ThreatActor);
|
||||
if (!ThreatPawn || !ThreatPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const EPS_AI_Behavior_NPCType TargetType =
|
||||
IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(ThreatPawn);
|
||||
|
||||
return DangerousTargetTypes.Contains(TargetType);
|
||||
}
|
||||
|
||||
FString UPS_AI_Behavior_BTDecorator_IsCoverNeeded::GetStaticDescription() const
|
||||
{
|
||||
const UEnum* TypeEnum = StaticEnum<EPS_AI_Behavior_NPCType>();
|
||||
FString TypeList;
|
||||
for (const EPS_AI_Behavior_NPCType& Type : DangerousTargetTypes)
|
||||
{
|
||||
if (!TypeList.IsEmpty()) TypeList += TEXT(", ");
|
||||
TypeList += TypeEnum->GetDisplayNameTextByValue(static_cast<int64>(Type)).ToString();
|
||||
}
|
||||
return FString::Printf(TEXT("Cover needed vs: %s"),
|
||||
TypeList.IsEmpty() ? TEXT("None") : *TypeList);
|
||||
}
|
||||
@ -12,6 +12,31 @@
|
||||
#include "Navigation/PathFollowingComponent.h"
|
||||
#include "CollisionQueryParams.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "EnvironmentQuery/EnvQuery.h"
|
||||
#include "EnvironmentQuery/EnvQueryManager.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
|
||||
// ─── Helper: Crouch at cover if required ────────────────────────────────────
|
||||
namespace
|
||||
{
|
||||
void CrouchAtCoverIfNeeded(APawn* Pawn, UBlackboardComponent* BB)
|
||||
{
|
||||
if (!Pawn || !Pawn->Implements<UPS_AI_Behavior_Interface>() || !BB) return;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void SetSubState(UBlackboardComponent* BB, EPS_AI_Behavior_CombatSubState State)
|
||||
{
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, static_cast<uint8>(State));
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Constructor ────────────────────────────────────────────────────────────
|
||||
|
||||
UPS_AI_Behavior_BTTask_CoverShootCycle::UPS_AI_Behavior_BTTask_CoverShootCycle()
|
||||
{
|
||||
@ -20,6 +45,8 @@ UPS_AI_Behavior_BTTask_CoverShootCycle::UPS_AI_Behavior_BTTask_CoverShootCycle()
|
||||
bNotifyTaskFinished = true;
|
||||
}
|
||||
|
||||
// ─── ExecuteTask ────────────────────────────────────────────────────────────
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::ExecuteTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
@ -29,7 +56,6 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::ExecuteTask(
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB) return EBTNodeResult::Failed;
|
||||
|
||||
// We need a cover location (written by BTTask_FindCover)
|
||||
const FVector CoverLoc = BB->GetValueAsVector(PS_AI_Behavior_BB::CoverLocation);
|
||||
if (CoverLoc.IsZero())
|
||||
{
|
||||
@ -37,7 +63,6 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::ExecuteTask(
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// We need a threat
|
||||
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||
if (!Target)
|
||||
{
|
||||
@ -45,13 +70,16 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::ExecuteTask(
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
// ─── Initialize memory with personality modulation ───────────────
|
||||
// ─── Initialize memory ──────────────────────────────────────────
|
||||
FCoverShootMemory* Memory = reinterpret_cast<FCoverShootMemory*>(NodeMemory);
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::Engaging;
|
||||
Memory->Timer = 0.0f;
|
||||
Memory->PhaseDuration = 0.0f;
|
||||
Memory->CycleCount = 0;
|
||||
Memory->bMoveRequested = false;
|
||||
Memory->FiringPosition = FVector::ZeroVector;
|
||||
Memory->bHasFiringPosition = false;
|
||||
Memory->bEQSRunning = false;
|
||||
|
||||
// Base values
|
||||
Memory->EffPeekMin = PeekDurationMin;
|
||||
@ -68,22 +96,19 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::ExecuteTask(
|
||||
const float Caution = Personality->GetTrait(EPS_AI_Behavior_TraitAxis::Caution);
|
||||
const float Courage = Personality->GetTrait(EPS_AI_Behavior_TraitAxis::Courage);
|
||||
|
||||
// Aggressive → peek longer, cover shorter, advance sooner
|
||||
const float AggrFactor = 0.7f + Aggressivity * 0.6f; // 0.7 – 1.3
|
||||
const float AggrFactor = 0.7f + Aggressivity * 0.6f;
|
||||
Memory->EffPeekMin *= AggrFactor;
|
||||
Memory->EffPeekMax *= AggrFactor;
|
||||
Memory->EffCoverMin /= AggrFactor;
|
||||
Memory->EffCoverMax /= AggrFactor;
|
||||
Memory->EffMaxCycles = FMath::Max(1, FMath::RoundToInt(MaxCyclesBeforeAdvance * (1.5f - Aggressivity * 0.5f)));
|
||||
|
||||
// Cautious → cover longer, peek shorter
|
||||
const float CautionFactor = 0.5f + Caution * 1.0f; // 0.5 – 1.5
|
||||
const float CautionFactor = 0.5f + Caution * 1.0f;
|
||||
Memory->EffCoverMin *= CautionFactor;
|
||||
Memory->EffCoverMax *= CautionFactor;
|
||||
Memory->EffPeekMin /= CautionFactor;
|
||||
Memory->EffPeekMax /= CautionFactor;
|
||||
|
||||
// Low courage → never advance
|
||||
Memory->bCanAdvance = (Courage >= 0.3f);
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||
@ -94,10 +119,9 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::ExecuteTask(
|
||||
Memory->EffMaxCycles, (int32)Memory->bCanAdvance);
|
||||
}
|
||||
|
||||
// ─── Move to cover position ──────────────────────────────────────
|
||||
// ─── Move to cover position ─────────────────────────────────────
|
||||
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
|
||||
CoverLoc, 80.0f, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
||||
CoverLoc, 80.0f, true, true, true, false);
|
||||
|
||||
if (Result == EPathFollowingRequestResult::Failed)
|
||||
{
|
||||
@ -106,23 +130,22 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::ExecuteTask(
|
||||
|
||||
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
|
||||
{
|
||||
// Already at cover — start the cycle
|
||||
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));
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::AtCover);
|
||||
}
|
||||
else
|
||||
{
|
||||
Memory->bMoveRequested = true;
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::Engaging));
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::Engaging);
|
||||
}
|
||||
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
// ─── TickTask ───────────────────────────────────────────────────────────────
|
||||
|
||||
void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
|
||||
{
|
||||
@ -139,13 +162,11 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||
if (!Target)
|
||||
{
|
||||
// De-escalate so decorator can re-trigger when threat returns
|
||||
AIC->SetBehaviorState(EPS_AI_Behavior_State::Alerted);
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate target
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
@ -153,7 +174,6 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
{
|
||||
AIC->StopMovement();
|
||||
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||
// De-escalate so decorator can re-trigger when threat returns
|
||||
AIC->SetBehaviorState(EPS_AI_Behavior_State::Alerted);
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
return;
|
||||
@ -162,6 +182,9 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
|
||||
FCoverShootMemory* Memory = reinterpret_cast<FCoverShootMemory*>(NodeMemory);
|
||||
|
||||
// Wait for EQS to complete
|
||||
if (Memory->bEQSRunning) return;
|
||||
|
||||
switch (Memory->SubState)
|
||||
{
|
||||
// ─── ENGAGING: Moving to cover ──────────────────────────────────
|
||||
@ -169,25 +192,12 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
{
|
||||
if (!Memory->bMoveRequested || AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
// Arrived at cover → start duck phase
|
||||
Memory->bMoveRequested = false;
|
||||
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));
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::AtCover);
|
||||
CrouchAtCoverIfNeeded(Pawn, BB);
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: at cover, ducking for %.1fs"),
|
||||
*AIC->GetName(), Memory->PhaseDuration);
|
||||
@ -201,83 +211,32 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
Memory->Timer -= DeltaSeconds;
|
||||
if (Memory->Timer <= 0.0f)
|
||||
{
|
||||
// LOS check before peeking — no point shooting into a wall
|
||||
const bool bHasLOS = UPS_AI_Behavior_Statics::HasLineOfSight(
|
||||
Pawn->GetWorld(), Pawn, Target, 150.0f);
|
||||
|
||||
if (!bHasLOS)
|
||||
{
|
||||
// No LOS → skip Peeking, force Advancing immediately to find a better position
|
||||
UE_LOG(LogPS_AI_Behavior, Log,
|
||||
TEXT("[%s] CoverShootCycle: no LOS to target from cover, skipping peek → advancing"),
|
||||
*AIC->GetName());
|
||||
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::Advancing;
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::Advancing));
|
||||
|
||||
// Release current cover point
|
||||
APS_AI_Behavior_CoverPoint* OldPoint =
|
||||
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
|
||||
if (OldPoint)
|
||||
{
|
||||
OldPoint->Release(Pawn);
|
||||
}
|
||||
|
||||
// Find a cover with better firing angle
|
||||
const FVector NpcLoc = Pawn->GetActorLocation();
|
||||
const FVector ThreatLoc = Target->GetActorLocation();
|
||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Any;
|
||||
if (UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent())
|
||||
{
|
||||
NPCType = Personality->GetNPCType();
|
||||
}
|
||||
|
||||
float NewScore = -1.0f;
|
||||
APS_AI_Behavior_CoverPoint* NewPoint =
|
||||
FindAdvancingCover(GetWorld(), NpcLoc, ThreatLoc, NPCType, NewScore);
|
||||
|
||||
if (NewPoint)
|
||||
{
|
||||
NewPoint->Claim(Pawn);
|
||||
BB->SetValueAsObject(PS_AI_Behavior_BB::CoverPoint, NewPoint);
|
||||
BB->SetValueAsVector(PS_AI_Behavior_BB::CoverLocation, NewPoint->GetActorLocation());
|
||||
|
||||
AIC->MoveToLocation(
|
||||
NewPoint->GetActorLocation(), 80.0f, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
||||
Memory->bMoveRequested = true;
|
||||
Memory->CycleCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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;
|
||||
// Try to find a firing position via EQS, or peek in place
|
||||
StartPeeking(OwnerComp, NodeMemory, AIC, Pawn, Target);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Has LOS → peek and shoot normally
|
||||
// ─── MOVING TO FIRE: Going to firing position ───────────────────
|
||||
case EPS_AI_Behavior_CombatSubState::MovingToFire:
|
||||
{
|
||||
if (!Memory->bMoveRequested || AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
// Arrived at firing position → start shooting
|
||||
Memory->bMoveRequested = false;
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::Peeking;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffPeekMin, Memory->EffPeekMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
Memory->LOSCheckTimer = 0.3f;
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::Peeking);
|
||||
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::Peeking));
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: peeking, shooting for %.1fs"),
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: at firing position, shooting for %.1fs"),
|
||||
*AIC->GetName(), Memory->PhaseDuration);
|
||||
}
|
||||
break;
|
||||
@ -286,36 +245,24 @@ 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
|
||||
// Continuous LOS check
|
||||
Memory->LOSCheckTimer -= DeltaSeconds;
|
||||
if (Memory->LOSCheckTimer <= 0.0f)
|
||||
{
|
||||
Memory->LOSCheckTimer = 0.3f; // check every 0.3s
|
||||
Memory->LOSCheckTimer = 0.3f;
|
||||
const bool bStillHasLOS = UPS_AI_Behavior_Statics::HasLineOfSight(
|
||||
Pawn->GetWorld(), Pawn, Target, 150.0f);
|
||||
if (!bStillHasLOS)
|
||||
{
|
||||
// Target hid — stop shooting, crouch back to cover
|
||||
// Lost LOS → stop shooting, return 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"),
|
||||
TEXT("[%s] CoverShootCycle: lost LOS during peek → returning 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));
|
||||
ReturnToCover(OwnerComp, NodeMemory, AIC, Pawn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -323,12 +270,11 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
Memory->Timer -= DeltaSeconds;
|
||||
if (Memory->Timer <= 0.0f)
|
||||
{
|
||||
// Stop attacking
|
||||
// Peek timer expired → stop shooting
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn);
|
||||
}
|
||||
|
||||
Memory->CycleCount++;
|
||||
|
||||
// Should we advance to closer cover?
|
||||
@ -336,18 +282,12 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
{
|
||||
// ─── Advance to next cover ──────────────────────
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::Advancing;
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::Advancing));
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::Advancing);
|
||||
|
||||
// Release current cover point
|
||||
APS_AI_Behavior_CoverPoint* OldPoint =
|
||||
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
|
||||
if (OldPoint)
|
||||
{
|
||||
OldPoint->Release(Pawn);
|
||||
}
|
||||
if (OldPoint) OldPoint->Release(Pawn);
|
||||
|
||||
// Find a closer cover
|
||||
const FVector NpcLoc = Pawn->GetActorLocation();
|
||||
const FVector ThreatLoc = Target->GetActorLocation();
|
||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Any;
|
||||
@ -365,10 +305,10 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
NewPoint->Claim(Pawn);
|
||||
BB->SetValueAsObject(PS_AI_Behavior_BB::CoverPoint, NewPoint);
|
||||
BB->SetValueAsVector(PS_AI_Behavior_BB::CoverLocation, NewPoint->GetActorLocation());
|
||||
Memory->bHasFiringPosition = false; // Reset firing position for new cover
|
||||
|
||||
AIC->MoveToLocation(
|
||||
NewPoint->GetActorLocation(), 80.0f, /*bStopOnOverlap=*/true,
|
||||
/*bUsePathfinding=*/true, /*bProjectGoal=*/true, /*bCanStrafe=*/false);
|
||||
NewPoint->GetActorLocation(), 80.0f, true, true, true, false);
|
||||
Memory->bMoveRequested = true;
|
||||
Memory->CycleCount = 0;
|
||||
|
||||
@ -377,43 +317,41 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
}
|
||||
else
|
||||
{
|
||||
// No better cover found — stay at current position, reset cycle
|
||||
// No better cover → return to current cover, reset cycle
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||
TEXT("[%s] CoverShootCycle: no advancing cover found, resetting cycle"),
|
||||
TEXT("[%s] CoverShootCycle: no advancing cover found, returning to cover"),
|
||||
*AIC->GetName());
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
ReturnToCover(OwnerComp, NodeMemory, AIC, Pawn);
|
||||
Memory->CycleCount = 0;
|
||||
BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::AtCover));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ─── Duck back behind cover ─────────────────────
|
||||
// ─── Return to cover ────────────────────────────
|
||||
ReturnToCover(OwnerComp, NodeMemory, AIC, Pawn);
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose,
|
||||
TEXT("[%s] CoverShootCycle: returning to cover (cycle %d/%d)"),
|
||||
*AIC->GetName(), Memory->CycleCount, Memory->EffMaxCycles);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ─── RETURNING TO COVER: Moving back after shooting ─────────────
|
||||
case EPS_AI_Behavior_CombatSubState::ReturningToCover:
|
||||
{
|
||||
if (!Memory->bMoveRequested || AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
Memory->bMoveRequested = false;
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::AtCover);
|
||||
CrouchAtCoverIfNeeded(Pawn, BB);
|
||||
|
||||
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);
|
||||
}
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: back at cover, ducking for %.1fs"),
|
||||
*AIC->GetName(), Memory->PhaseDuration);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -423,25 +361,12 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
{
|
||||
if (!Memory->bMoveRequested || AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
// Arrived at new cover → duck
|
||||
Memory->bMoveRequested = false;
|
||||
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));
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::AtCover);
|
||||
CrouchAtCoverIfNeeded(Pawn, BB);
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: arrived at new cover, ducking"),
|
||||
*AIC->GetName());
|
||||
@ -449,7 +374,302 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::TickTask(
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Debug visualization ────────────────────────────────────────
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
if (bDebugDraw && Pawn && Target)
|
||||
{
|
||||
UWorld* World = Pawn->GetWorld();
|
||||
const FVector HeadLoc = Pawn->GetActorLocation() + FVector(0, 0, 120.0f);
|
||||
const FVector TargetLoc = Target->GetActorLocation();
|
||||
const FVector CoverLoc = BB->GetValueAsVector(PS_AI_Behavior_BB::CoverLocation);
|
||||
|
||||
// Sub-state label
|
||||
const TCHAR* SubStateStr = TEXT("?");
|
||||
FColor SubStateColor = FColor::White;
|
||||
switch (Memory->SubState)
|
||||
{
|
||||
case EPS_AI_Behavior_CombatSubState::Engaging: SubStateStr = TEXT("ENGAGING"); SubStateColor = FColor::Yellow; break;
|
||||
case EPS_AI_Behavior_CombatSubState::AtCover: SubStateStr = TEXT("AT COVER"); SubStateColor = FColor::Cyan; break;
|
||||
case EPS_AI_Behavior_CombatSubState::MovingToFire: SubStateStr = TEXT("→ FIRE POS"); SubStateColor = FColor::Magenta; break;
|
||||
case EPS_AI_Behavior_CombatSubState::Peeking: SubStateStr = TEXT("PEEKING"); SubStateColor = FColor::Red; break;
|
||||
case EPS_AI_Behavior_CombatSubState::ReturningToCover: SubStateStr = TEXT("→ COVER"); SubStateColor = FColor::Blue; break;
|
||||
case EPS_AI_Behavior_CombatSubState::Advancing: SubStateStr = TEXT("ADVANCING"); SubStateColor = FColor::Orange; break;
|
||||
}
|
||||
DrawDebugString(World, HeadLoc + FVector(0, 0, 30.0f),
|
||||
FString::Printf(TEXT("%s [%.1fs] C:%d/%d"), SubStateStr, Memory->Timer, Memory->CycleCount, Memory->EffMaxCycles),
|
||||
nullptr, SubStateColor, 0.0f, true);
|
||||
|
||||
// LOS line to target
|
||||
const bool bLOS = UPS_AI_Behavior_Statics::HasLineOfSight(World, Pawn, Target, 150.0f);
|
||||
DrawDebugLine(World, HeadLoc, TargetLoc + FVector(0, 0, 100.0f),
|
||||
bLOS ? FColor::Green : FColor::Red, false, 0.0f, 0, 1.0f);
|
||||
|
||||
// Firing position marker
|
||||
if (Memory->bHasFiringPosition)
|
||||
{
|
||||
// Firing position: red solid point
|
||||
DrawDebugSphere(World, Memory->FiringPosition + FVector(0, 0, 30.0f),
|
||||
20.0f, 8, FColor::Red, false, 0.0f);
|
||||
// Line cover → firing position (white)
|
||||
DrawDebugLine(World, CoverLoc + FVector(0, 0, 20.0f),
|
||||
Memory->FiringPosition + FVector(0, 0, 20.0f),
|
||||
FColor::White, false, 0.0f, 0, 1.0f);
|
||||
// Line firing position → threat (green)
|
||||
DrawDebugLine(World, Memory->FiringPosition + FVector(0, 0, 100.0f),
|
||||
TargetLoc + FVector(0, 0, 100.0f),
|
||||
FColor(0, 200, 0), false, 0.0f, 0, 1.0f);
|
||||
DrawDebugString(World, Memory->FiringPosition + FVector(0, 0, 50.0f),
|
||||
TEXT("FIRE"), nullptr, FColor::Red, 0.0f, true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ─── StartPeeking ───────────────────────────────────────────────────────────
|
||||
|
||||
void UPS_AI_Behavior_BTTask_CoverShootCycle::StartPeeking(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory,
|
||||
APS_AI_Behavior_AIController* AIC, APawn* Pawn, AActor* Target)
|
||||
{
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
FCoverShootMemory* Memory = reinterpret_cast<FCoverShootMemory*>(NodeMemory);
|
||||
|
||||
// If we have a FiringPositionQuery → find a firing position via EQS
|
||||
if (FiringPositionQuery)
|
||||
{
|
||||
RunFiringPositionQuery(OwnerComp, NodeMemory, Pawn);
|
||||
return; // EQS callback will handle the transition
|
||||
}
|
||||
|
||||
// No EQS query → legacy behavior: check LOS from cover and shoot in place
|
||||
const bool bHasLOS = UPS_AI_Behavior_Statics::HasLineOfSight(
|
||||
Pawn->GetWorld(), Pawn, Target, 150.0f);
|
||||
|
||||
if (!bHasLOS)
|
||||
{
|
||||
// No LOS and no firing position query → try to advance
|
||||
UE_LOG(LogPS_AI_Behavior, Log,
|
||||
TEXT("[%s] CoverShootCycle: no LOS from cover and no FiringPositionQuery → advancing"),
|
||||
*AIC->GetName());
|
||||
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::Advancing;
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::Advancing);
|
||||
|
||||
APS_AI_Behavior_CoverPoint* OldPoint =
|
||||
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
|
||||
if (OldPoint) OldPoint->Release(Pawn);
|
||||
|
||||
const FVector NpcLoc = Pawn->GetActorLocation();
|
||||
const FVector ThreatLoc = Target->GetActorLocation();
|
||||
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Any;
|
||||
if (UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent())
|
||||
{
|
||||
NPCType = Personality->GetNPCType();
|
||||
}
|
||||
|
||||
float NewScore = -1.0f;
|
||||
APS_AI_Behavior_CoverPoint* NewPoint =
|
||||
FindAdvancingCover(GetWorld(), NpcLoc, ThreatLoc, NPCType, NewScore);
|
||||
|
||||
if (NewPoint)
|
||||
{
|
||||
NewPoint->Claim(Pawn);
|
||||
BB->SetValueAsObject(PS_AI_Behavior_BB::CoverPoint, NewPoint);
|
||||
BB->SetValueAsVector(PS_AI_Behavior_BB::CoverLocation, NewPoint->GetActorLocation());
|
||||
AIC->MoveToLocation(NewPoint->GetActorLocation(), 80.0f, true, true, true, false);
|
||||
Memory->bMoveRequested = true;
|
||||
Memory->CycleCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log,
|
||||
TEXT("[%s] CoverShootCycle: no LOS and no advancing cover → abandoning cover"),
|
||||
*AIC->GetName());
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Has LOS → shoot in place (legacy)
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::Peeking;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffPeekMin, Memory->EffPeekMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
Memory->LOSCheckTimer = 0.3f;
|
||||
Memory->bHasFiringPosition = false;
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::Peeking);
|
||||
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, false);
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: peeking in place for %.1fs"),
|
||||
*AIC->GetName(), Memory->PhaseDuration);
|
||||
}
|
||||
|
||||
// ─── ReturnToCover ──────────────────────────────────────────────────────────
|
||||
|
||||
void UPS_AI_Behavior_BTTask_CoverShootCycle::ReturnToCover(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory,
|
||||
APS_AI_Behavior_AIController* AIC, APawn* Pawn)
|
||||
{
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
FCoverShootMemory* Memory = reinterpret_cast<FCoverShootMemory*>(NodeMemory);
|
||||
|
||||
// If we have a firing position (we moved away from cover), move back
|
||||
if (Memory->bHasFiringPosition)
|
||||
{
|
||||
const FVector CoverLoc = BB->GetValueAsVector(PS_AI_Behavior_BB::CoverLocation);
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::ReturningToCover;
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::ReturningToCover);
|
||||
|
||||
AIC->MoveToLocation(CoverLoc, 80.0f, true, true, true, false);
|
||||
Memory->bMoveRequested = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No firing position was used → already at cover, duck directly
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::AtCover);
|
||||
CrouchAtCoverIfNeeded(Pawn, BB);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── EQS Firing Position ────────────────────────────────────────────────────
|
||||
|
||||
void UPS_AI_Behavior_BTTask_CoverShootCycle::RunFiringPositionQuery(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, APawn* Pawn)
|
||||
{
|
||||
FCoverShootMemory* Memory = reinterpret_cast<FCoverShootMemory*>(NodeMemory);
|
||||
|
||||
UWorld* World = Pawn->GetWorld();
|
||||
UEnvQueryManager* EQSManager = UEnvQueryManager::GetCurrent(World);
|
||||
if (!EQSManager)
|
||||
{
|
||||
// Fallback: peek in place
|
||||
Memory->bHasFiringPosition = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Memory->bEQSRunning = true;
|
||||
|
||||
FEnvQueryRequest Request(FiringPositionQuery, Pawn);
|
||||
Request.Execute(EEnvQueryRunMode::SingleResult,
|
||||
FQueryFinishedSignature::CreateUObject(this,
|
||||
&UPS_AI_Behavior_BTTask_CoverShootCycle::OnFiringPositionQueryFinished,
|
||||
&OwnerComp, NodeMemory));
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: firing position EQS launched"),
|
||||
*Pawn->GetName());
|
||||
}
|
||||
|
||||
void UPS_AI_Behavior_BTTask_CoverShootCycle::OnFiringPositionQueryFinished(
|
||||
TSharedPtr<FEnvQueryResult> Result,
|
||||
UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory)
|
||||
{
|
||||
if (!OwnerComp || !NodeMemory) return;
|
||||
|
||||
FCoverShootMemory* Memory = reinterpret_cast<FCoverShootMemory*>(NodeMemory);
|
||||
Memory->bEQSRunning = false;
|
||||
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp->GetAIOwner());
|
||||
if (!AIC || !AIC->GetPawn()) return;
|
||||
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
UBlackboardComponent* BB = OwnerComp->GetBlackboardComponent();
|
||||
if (!BB) return;
|
||||
|
||||
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||
|
||||
if (Result.IsValid() && Result->IsSuccessful())
|
||||
{
|
||||
// Found a firing position → move to it
|
||||
Memory->FiringPosition = Result->GetItemAsLocation(0);
|
||||
Memory->bHasFiringPosition = true;
|
||||
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::MovingToFire;
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::MovingToFire);
|
||||
|
||||
// Stand up to move to firing position
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, false);
|
||||
}
|
||||
|
||||
AIC->MoveToLocation(Memory->FiringPosition, 50.0f, true, true, true, false);
|
||||
Memory->bMoveRequested = true;
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] CoverShootCycle: moving to firing position %s"),
|
||||
*AIC->GetName(), *Memory->FiringPosition.ToString());
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
if (bDebugDraw)
|
||||
{
|
||||
UWorld* World = Pawn->GetWorld();
|
||||
// Draw all EQS firing candidates as solid boxes
|
||||
const int32 NumItems = Result->Items.Num();
|
||||
for (int32 i = 0; i < NumItems; ++i)
|
||||
{
|
||||
if (!Result->Items[i].IsValid()) continue;
|
||||
const FVector ItemLoc = Result->GetItemAsLocation(i);
|
||||
const float ItemScore = Result->GetItemScore(i);
|
||||
const uint8 G = static_cast<uint8>(FMath::Lerp(50.0f, 255.0f, FMath::Clamp(ItemScore, 0.0f, 1.0f)));
|
||||
DrawDebugBox(World, ItemLoc + FVector(0, 0, 20.0f),
|
||||
FVector(8.0f), FColor(255, G, 0), false, 5.0f);
|
||||
}
|
||||
// Chosen firing position: large red point
|
||||
DrawDebugSphere(World, Memory->FiringPosition + FVector(0, 0, 30.0f),
|
||||
25.0f, 8, FColor::Red, false, 5.0f);
|
||||
DrawDebugString(World, Memory->FiringPosition + FVector(0, 0, 55.0f),
|
||||
FString::Printf(TEXT("FIRE POS (%d)"), NumItems),
|
||||
nullptr, FColor::Red, 5.0f, true);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
// EQS failed → fallback: peek in place if LOS exists
|
||||
Memory->bHasFiringPosition = false;
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] CoverShootCycle: firing position EQS failed, peeking in place"),
|
||||
*AIC->GetName());
|
||||
|
||||
if (Target)
|
||||
{
|
||||
const bool bHasLOS = UPS_AI_Behavior_Statics::HasLineOfSight(
|
||||
Pawn->GetWorld(), Pawn, Target, 150.0f);
|
||||
|
||||
if (bHasLOS)
|
||||
{
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::Peeking;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffPeekMin, Memory->EffPeekMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
Memory->LOSCheckTimer = 0.3f;
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::Peeking);
|
||||
|
||||
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, false);
|
||||
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No LOS, no firing position → stay in cover, wait
|
||||
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;
|
||||
Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax);
|
||||
Memory->Timer = Memory->PhaseDuration;
|
||||
SetSubState(BB, EPS_AI_Behavior_CombatSubState::AtCover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── AbortTask ──────────────────────────────────────────────────────────────
|
||||
|
||||
EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::AbortTask(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
|
||||
@ -459,7 +679,6 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::AbortTask(
|
||||
{
|
||||
AIC->StopMovement();
|
||||
|
||||
// Stop attacking and stand up if we were peeking/crouching
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
@ -471,13 +690,14 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::AbortTask(
|
||||
return EBTNodeResult::Aborted;
|
||||
}
|
||||
|
||||
// ─── OnTaskFinished ─────────────────────────────────────────────────────────
|
||||
|
||||
void UPS_AI_Behavior_BTTask_CoverShootCycle::OnTaskFinished(
|
||||
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult)
|
||||
{
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||
if (AIC)
|
||||
{
|
||||
// Stop attacking and stand up
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
@ -485,7 +705,6 @@ void UPS_AI_Behavior_BTTask_CoverShootCycle::OnTaskFinished(
|
||||
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, false);
|
||||
}
|
||||
|
||||
// Release cover point
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (BB)
|
||||
{
|
||||
@ -516,39 +735,28 @@ APS_AI_Behavior_CoverPoint* UPS_AI_Behavior_BTTask_CoverShootCycle::FindAdvancin
|
||||
{
|
||||
APS_AI_Behavior_CoverPoint* Point = *It;
|
||||
if (!Point || !Point->bEnabled) continue;
|
||||
|
||||
// Type filter
|
||||
if (Point->PointType != CoverPointType) continue;
|
||||
|
||||
// NPC type accessibility
|
||||
if (!Point->IsAccessibleTo(NPCType)) continue;
|
||||
|
||||
// Availability
|
||||
if (!Point->HasRoom()) continue;
|
||||
|
||||
// Distance check from NPC
|
||||
const float DistFromNpc = FVector::Dist(NpcLoc, Point->GetActorLocation());
|
||||
if (DistFromNpc > AdvanceSearchRadius) continue;
|
||||
|
||||
// Must be closer to threat than NPC currently is
|
||||
const float CoverDistToThreat = FVector::Dist(Point->GetActorLocation(), ThreatLoc);
|
||||
if (CoverDistToThreat >= NpcDistToThreat) continue;
|
||||
|
||||
// Evaluate quality against threat
|
||||
float Score = Point->EvaluateAgainstThreat(ThreatLoc);
|
||||
|
||||
// Distance bonus — closer to NPC is better (less travel time)
|
||||
Score += FMath::GetMappedRangeValueClamped(
|
||||
FVector2D(0.0f, AdvanceSearchRadius), FVector2D(0.15f, 0.0f), DistFromNpc);
|
||||
|
||||
// Advancement bonus — how much closer to threat this cover gets us
|
||||
if (NpcDistToThreat > 0.0f)
|
||||
{
|
||||
const float AdvanceRatio = (NpcDistToThreat - CoverDistToThreat) / NpcDistToThreat;
|
||||
Score += AdvancementBias * AdvanceRatio * 0.3f;
|
||||
}
|
||||
|
||||
// LOS bonus — strongly favor covers with clear line of sight to the target
|
||||
// LOS bonus — favor covers near positions with LOS to threat
|
||||
{
|
||||
const FVector TraceStart = Point->GetActorLocation() + FVector(0, 0, 150.0f);
|
||||
FHitResult Hit;
|
||||
@ -556,7 +764,7 @@ APS_AI_Behavior_CoverPoint* UPS_AI_Behavior_BTTask_CoverShootCycle::FindAdvancin
|
||||
if (!const_cast<UWorld*>(World)->LineTraceSingleByChannel(
|
||||
Hit, TraceStart, ThreatLoc, ECC_Visibility, Params))
|
||||
{
|
||||
Score += 0.3f; // Clear LOS from this cover
|
||||
Score += 0.3f;
|
||||
}
|
||||
}
|
||||
|
||||
@ -570,11 +778,14 @@ APS_AI_Behavior_CoverPoint* UPS_AI_Behavior_BTTask_CoverShootCycle::FindAdvancin
|
||||
return BestPoint;
|
||||
}
|
||||
|
||||
// ─── GetStaticDescription ───────────────────────────────────────────────────
|
||||
|
||||
FString UPS_AI_Behavior_BTTask_CoverShootCycle::GetStaticDescription() const
|
||||
{
|
||||
return FString::Printf(
|
||||
TEXT("Cover-shoot cycle.\nPeek: %.1f–%.1fs | Cover: %.1f–%.1fs\nAdvance after %d cycles (radius %.0fcm)"),
|
||||
TEXT("Cover-shoot cycle.\nPeek: %.1f–%.1fs | Cover: %.1f–%.1fs\nAdvance after %d cycles (radius %.0fcm)\nFiring EQS: %s"),
|
||||
PeekDurationMin, PeekDurationMax,
|
||||
CoverDurationMin, CoverDurationMax,
|
||||
MaxCyclesBeforeAdvance, AdvanceSearchRadius);
|
||||
MaxCyclesBeforeAdvance, AdvanceSearchRadius,
|
||||
FiringPositionQuery ? *FiringPositionQuery->GetName() : TEXT("None (in-place)"));
|
||||
}
|
||||
|
||||
@ -12,7 +12,21 @@
|
||||
#include "CollisionQueryParams.h"
|
||||
#include "Engine/World.h"
|
||||
#include "EngineUtils.h"
|
||||
#include "EnvironmentQuery/EnvQuery.h"
|
||||
#include "EnvironmentQuery/EnvQueryManager.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
FColor ScoreToColor(float Score)
|
||||
{
|
||||
// 0 = red, 0.5 = yellow, 1 = green
|
||||
const float Clamped = FMath::Clamp(Score, 0.0f, 1.0f);
|
||||
const uint8 R = static_cast<uint8>(FMath::Lerp(255.0f, 0.0f, Clamped));
|
||||
const uint8 G = static_cast<uint8>(FMath::Lerp(0.0f, 255.0f, Clamped));
|
||||
return FColor(R, G, 0);
|
||||
}
|
||||
}
|
||||
|
||||
UPS_AI_Behavior_BTTask_FindCover::UPS_AI_Behavior_BTTask_FindCover()
|
||||
{
|
||||
@ -85,6 +99,17 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindCover::ExecuteTask(
|
||||
}
|
||||
|
||||
const float Score = EvaluateCoverQuality(World, NavLoc.Location, ThreatLoc, NpcLoc);
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
if (bDebugDraw)
|
||||
{
|
||||
DrawDebugSphere(World, NavLoc.Location + FVector(0, 0, 30.0f),
|
||||
15.0f, 6, ScoreToColor(Score), false, 5.0f);
|
||||
DrawDebugString(World, NavLoc.Location + FVector(0, 0, 55.0f),
|
||||
FString::Printf(TEXT("%.2f"), Score), nullptr, FColor::White, 5.0f, true);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (Score > BestScore)
|
||||
{
|
||||
BestScore = Score;
|
||||
@ -122,6 +147,25 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindCover::ExecuteTask(
|
||||
|
||||
BB->SetValueAsVector(PS_AI_Behavior_BB::CoverLocation, BestCoverPos);
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
if (bDebugDraw)
|
||||
{
|
||||
// Search radius circle
|
||||
DrawDebugCircle(World, NpcLoc, SearchRadius, 32, FColor(80, 80, 80),
|
||||
false, 5.0f, 0, 1.0f, FVector::RightVector, FVector::ForwardVector);
|
||||
// Chosen cover: large sphere + line from NPC
|
||||
DrawDebugSphere(World, BestCoverPos + FVector(0, 0, 40.0f),
|
||||
30.0f, 10, FColor::Cyan, false, 5.0f);
|
||||
DrawDebugLine(World, NpcLoc, BestCoverPos, FColor::Cyan, false, 5.0f, 0, 1.5f);
|
||||
DrawDebugString(World, BestCoverPos + FVector(0, 0, 65.0f),
|
||||
FString::Printf(TEXT("COVER %.2f %s"), BestScore,
|
||||
ChosenPoint ? TEXT("(Manual)") : TEXT("(Procedural)")),
|
||||
nullptr, FColor::Cyan, 5.0f, true);
|
||||
// Line to threat
|
||||
DrawDebugLine(World, BestCoverPos, ThreatLoc, FColor(255, 100, 0), false, 5.0f, 0, 1.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
FCoverMemory* Memory = reinterpret_cast<FCoverMemory*>(NodeMemory);
|
||||
|
||||
// If we have a refinement EQS query and a manual CoverPoint, refine the position
|
||||
@ -262,6 +306,18 @@ APS_AI_Behavior_CoverPoint* UPS_AI_Behavior_BTTask_FindCover::FindBestManualCove
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: sphere colored by score (cover candidates)
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
if (bDebugDraw)
|
||||
{
|
||||
const FVector Loc = Point->GetActorLocation();
|
||||
DrawDebugSphere(const_cast<UWorld*>(World), Loc + FVector(0, 0, 30.0f),
|
||||
15.0f, 6, ScoreToColor(Score), false, 5.0f);
|
||||
DrawDebugString(const_cast<UWorld*>(World), Loc + FVector(0, 0, 55.0f),
|
||||
FString::Printf(TEXT("%.2f"), Score), nullptr, FColor::White, 5.0f, true);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (Score > OutScore)
|
||||
{
|
||||
OutScore = Score;
|
||||
@ -383,6 +439,38 @@ void UPS_AI_Behavior_BTTask_FindCover::OnRefinementQueryFinished(
|
||||
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());
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
if (bDebugDraw)
|
||||
{
|
||||
UWorld* World = AIC->GetWorld();
|
||||
if (World)
|
||||
{
|
||||
// Draw EQS refinement items as BOXES (cover = spheres, refinement = boxes)
|
||||
const int32 NumItems = Result->Items.Num();
|
||||
for (int32 i = 0; i < NumItems; ++i)
|
||||
{
|
||||
if (!Result->Items[i].IsValid()) continue;
|
||||
const FVector ItemLoc = Result->GetItemAsLocation(i);
|
||||
const float ItemScore = Result->GetItemScore(i);
|
||||
DrawDebugBox(World, ItemLoc + FVector(0, 0, 20.0f),
|
||||
FVector(8.0f), ScoreToColor(ItemScore), false, 5.0f);
|
||||
}
|
||||
// Original position (yellow box)
|
||||
DrawDebugBox(World, OriginalCoverPos + FVector(0, 0, 50.0f),
|
||||
FVector(15.0f), FColor::Yellow, false, 5.0f, 0, 2.0f);
|
||||
// Refined position (green box, bigger)
|
||||
DrawDebugBox(World, FinalPos + FVector(0, 0, 50.0f),
|
||||
FVector(20.0f), FColor::Green, false, 5.0f, 0, 2.5f);
|
||||
// Arrow from original to refined
|
||||
DrawDebugDirectionalArrow(World, OriginalCoverPos + FVector(0, 0, 50.0f),
|
||||
FinalPos + FVector(0, 0, 50.0f), 10.0f, FColor::Green, false, 5.0f, 0, 1.5f);
|
||||
DrawDebugString(World, FinalPos + FVector(0, 0, 75.0f),
|
||||
FString::Printf(TEXT("REFINED (%d)"), NumItems),
|
||||
nullptr, FColor::Green, 5.0f, true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#include "EQS/PS_AI_Behavior_EQSContext_CoverLocation.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "EnvironmentQuery/EnvQueryTypes.h"
|
||||
#include "EnvironmentQuery/Items/EnvQueryItemType_Point.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
|
||||
void UPS_AI_Behavior_EQSContext_CoverLocation::ProvideContext(
|
||||
FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const
|
||||
{
|
||||
const AActor* QuerierActor = Cast<AActor>(QueryInstance.Owner.Get());
|
||||
if (!QuerierActor) return;
|
||||
|
||||
const APawn* QuerierPawn = Cast<APawn>(QuerierActor);
|
||||
if (!QuerierPawn) return;
|
||||
|
||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(QuerierPawn->GetController());
|
||||
if (!AIC) return;
|
||||
|
||||
UBlackboardComponent* BB = AIC->GetBlackboardComponent();
|
||||
if (!BB) return;
|
||||
|
||||
const FVector CoverLoc = BB->GetValueAsVector(PS_AI_Behavior_BB::CoverLocation);
|
||||
if (!CoverLoc.IsZero())
|
||||
{
|
||||
UEnvQueryItemType_Point::SetContextHelper(ContextData, CoverLoc);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTDecorator.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_BTDecorator_IsCoverNeeded.generated.h"
|
||||
|
||||
/**
|
||||
* BT Decorator: Checks whether the current threat target warrants taking cover.
|
||||
*
|
||||
* Reads the ThreatActor from Blackboard and inspects its NPCType.
|
||||
* Cover is considered necessary against armed/dangerous targets (Protector, Enemy)
|
||||
* but not against unarmed targets (Civilian).
|
||||
*
|
||||
* Place on the cover-shoot sequence so that enemies skip cover when chasing civilians.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Is Cover Needed"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTDecorator_IsCoverNeeded : public UBTDecorator
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPS_AI_Behavior_BTDecorator_IsCoverNeeded();
|
||||
|
||||
/**
|
||||
* NPC types that are considered dangerous enough to require cover.
|
||||
* If the threat's NPCType is NOT in this set, the decorator fails → skip cover.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category = "Cover", meta = (Bitmask, BitmaskEnum = "/Script/PS_AI_Behavior.EPS_AI_Behavior_NPCType"))
|
||||
TArray<EPS_AI_Behavior_NPCType> DangerousTargetTypes;
|
||||
|
||||
protected:
|
||||
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
|
||||
virtual FString GetStaticDescription() const override;
|
||||
};
|
||||
@ -4,19 +4,24 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "BehaviorTree/BTTaskNode.h"
|
||||
#include "EnvironmentQuery/EnvQueryTypes.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "PS_AI_Behavior_BTTask_CoverShootCycle.generated.h"
|
||||
|
||||
class APS_AI_Behavior_AIController;
|
||||
class APS_AI_Behavior_CoverPoint;
|
||||
class UEnvQuery;
|
||||
|
||||
/**
|
||||
* BT Task: Cover-shoot cycle for ranged combat.
|
||||
*
|
||||
* State machine: Engaging → AtCover → Peeking → AtCover → ... → Advancing → AtCover ...
|
||||
* State machine:
|
||||
* Engaging → AtCover (crouch) → [EQS] → MovingToFire → Peeking (shoot)
|
||||
* → ReturningToCover → AtCover ... → Advancing → AtCover ...
|
||||
*
|
||||
* The NPC moves to the cover position from Blackboard, then alternates between
|
||||
* ducking (AtCover) and shooting (Peeking). After MaxCyclesBeforeAdvance peek/duck
|
||||
* cycles, advances to a closer cover point toward the threat.
|
||||
* If FiringPositionQuery is set, the NPC physically moves between the cover
|
||||
* position (protected) and a nearby firing position (with LOS to threat).
|
||||
* If null, the NPC shoots from cover (legacy behavior: stand up and fire in place).
|
||||
*
|
||||
* Personality traits modulate timing:
|
||||
* - Aggressivity → shorter cover duration, advances sooner
|
||||
@ -54,6 +59,17 @@ public:
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
|
||||
float CoverDurationMax = 3.0f;
|
||||
|
||||
// ─── Firing Position ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Optional EQS query to find a firing position near the cover.
|
||||
* Should return positions with LOS to threat, close to CoverLocation.
|
||||
* Use OnCircle generator around PS AI: Cover Location + LineOfSight filter + Distance score.
|
||||
* If null, the NPC shoots from cover (stand up in place — legacy fallback).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Firing Position")
|
||||
TObjectPtr<UEnvQuery> FiringPositionQuery;
|
||||
|
||||
// ─── Advancement ────────────────────────────────────────────────────
|
||||
|
||||
/** Number of peek/duck cycles before advancing to a closer cover. */
|
||||
@ -72,6 +88,12 @@ public:
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Advancement")
|
||||
EPS_AI_Behavior_CoverPointType CoverPointType = EPS_AI_Behavior_CoverPointType::Cover;
|
||||
|
||||
// ─── Debug ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Draw debug info: sub-state label, LOS line, cycle counter, firing position. */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Debug")
|
||||
bool bDebugDraw = false;
|
||||
|
||||
protected:
|
||||
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
|
||||
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
|
||||
@ -87,7 +109,12 @@ private:
|
||||
float PhaseDuration = 0.0f;
|
||||
int32 CycleCount = 0;
|
||||
bool bMoveRequested = false;
|
||||
float LOSCheckTimer = 0.0f; // cooldown for LOS checks during Peeking
|
||||
float LOSCheckTimer = 0.0f;
|
||||
|
||||
// Firing position (found via EQS)
|
||||
FVector FiringPosition = FVector::ZeroVector;
|
||||
bool bHasFiringPosition = false;
|
||||
bool bEQSRunning = false;
|
||||
|
||||
// Effective durations (modulated by personality)
|
||||
float EffPeekMin = 2.0f;
|
||||
@ -104,4 +131,19 @@ private:
|
||||
APS_AI_Behavior_CoverPoint* FindAdvancingCover(
|
||||
const UWorld* World, const FVector& NpcLoc, const FVector& ThreatLoc,
|
||||
EPS_AI_Behavior_NPCType NPCType, float& OutScore) const;
|
||||
|
||||
/** Run EQS to find the best firing position near the cover. */
|
||||
void RunFiringPositionQuery(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, APawn* Pawn);
|
||||
|
||||
/** Callback when the firing position EQS query completes. */
|
||||
void OnFiringPositionQueryFinished(TSharedPtr<FEnvQueryResult> Result,
|
||||
UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory);
|
||||
|
||||
/** Transition to peeking: move to firing position or shoot in place. */
|
||||
void StartPeeking(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory,
|
||||
APS_AI_Behavior_AIController* AIC, APawn* Pawn, AActor* Target);
|
||||
|
||||
/** Transition back to cover after shooting. */
|
||||
void ReturnToCover(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory,
|
||||
APS_AI_Behavior_AIController* AIC, APawn* Pawn);
|
||||
};
|
||||
|
||||
@ -81,6 +81,12 @@ public:
|
||||
UPROPERTY(EditAnywhere, Category = "Cover|EQS Refinement")
|
||||
TObjectPtr<UEnvQuery> RefinementQuery;
|
||||
|
||||
// ─── Debug ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Draw debug spheres showing cover candidates and their scores at runtime. */
|
||||
UPROPERTY(EditAnywhere, Category = "Cover|Debug")
|
||||
bool bDebugDraw = false;
|
||||
|
||||
/** 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;
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
// Copyright Asterion. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "EnvironmentQuery/EnvQueryContext.h"
|
||||
#include "PS_AI_Behavior_EQSContext_CoverLocation.generated.h"
|
||||
|
||||
/**
|
||||
* EQS Context: Returns the current CoverLocation from the Blackboard.
|
||||
* Use as the "Generate Around" context in EQS queries (e.g., OnCircle generator)
|
||||
* to generate refinement candidates around the chosen CoverPoint instead of around the Querier.
|
||||
*/
|
||||
UCLASS(meta = (DisplayName = "PS AI: Cover Location"))
|
||||
class PS_AI_BEHAVIOR_API UPS_AI_Behavior_EQSContext_CoverLocation : public UEnvQueryContext
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void ProvideContext(FEnvQueryInstance& QueryInstance,
|
||||
FEnvQueryContextData& ContextData) const override;
|
||||
};
|
||||
@ -84,7 +84,9 @@ enum class EPS_AI_Behavior_CombatSubState : uint8
|
||||
{
|
||||
Engaging UMETA(DisplayName = "Engaging", ToolTip = "Moving to combat position"),
|
||||
AtCover UMETA(DisplayName = "At Cover", ToolTip = "Ducked behind cover"),
|
||||
Peeking UMETA(DisplayName = "Peeking", ToolTip = "Leaning out, shooting"),
|
||||
MovingToFire UMETA(DisplayName = "Moving To Fire", ToolTip = "Moving to firing position"),
|
||||
Peeking UMETA(DisplayName = "Peeking", ToolTip = "At firing position, shooting"),
|
||||
ReturningToCover UMETA(DisplayName = "Returning To Cover", ToolTip = "Moving back to cover after shooting"),
|
||||
Advancing UMETA(DisplayName = "Advancing", ToolTip = "Moving to next cover"),
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user