diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.dll b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.dll index 217d11c..6f94b08 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.dll and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.dll differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.exp index 469e727..51a7b0f 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.exp and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.exp differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.pdb index c7b13de..c2eede2 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.pdb and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_Behavior.pdb differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.dll b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.dll index 80fa687..ad30d52 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.dll and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.dll differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.exp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.exp index 4f83f9f..f816f59 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.exp and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.exp differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.pdb b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.pdb index d2f3e47..9c2691f 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.pdb and b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Binaries/Win64/UnrealEditor-PS_AI_BehaviorEditor.pdb differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTDecorator_IsCoverNeeded.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTDecorator_IsCoverNeeded.cpp new file mode 100644 index 0000000..6febf3c --- /dev/null +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTDecorator_IsCoverNeeded.cpp @@ -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(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(ThreatActor); + if (!ThreatPawn || !ThreatPawn->Implements()) + { + 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(); + FString TypeList; + for (const EPS_AI_Behavior_NPCType& Type : DangerousTargetTypes) + { + if (!TypeList.IsEmpty()) TypeList += TEXT(", "); + TypeList += TypeEnum->GetDisplayNameTextByValue(static_cast(Type)).ToString(); + } + return FString::Printf(TEXT("Cover needed vs: %s"), + TypeList.IsEmpty() ? TEXT("None") : *TypeList); +} diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_CoverShootCycle.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_CoverShootCycle.cpp index 260b869..2b49400 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_CoverShootCycle.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_CoverShootCycle.cpp @@ -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() || !BB) return; + const APS_AI_Behavior_CoverPoint* CoverPt = + Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); + if (CoverPt && CoverPt->bCrouch) + { + IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); + } + } + + void SetSubState(UBlackboardComponent* BB, EPS_AI_Behavior_CombatSubState State) + { + BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, static_cast(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(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(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(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(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(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()) { @@ -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(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(EPS_AI_Behavior_CombatSubState::AtCover)); - - // Crouch if the cover point requires it - if (Pawn->Implements()) - { - const APS_AI_Behavior_CoverPoint* CoverPt = - Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); - if (CoverPt && CoverPt->bCrouch) - { - IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); - } - } + 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); + // Try to find a firing position via EQS, or peek in place + StartPeeking(OwnerComp, NodeMemory, AIC, Pawn, Target); + } + break; + } - 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(EPS_AI_Behavior_CombatSubState::Advancing)); - - // Release current cover point - APS_AI_Behavior_CoverPoint* OldPoint = - Cast(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; - } - 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(EPS_AI_Behavior_CombatSubState::Peeking)); - - // Stand up to shoot if (Pawn->Implements()) { 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()) { IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn); - const APS_AI_Behavior_CoverPoint* CoverPt = - Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); - if (CoverPt && CoverPt->bCrouch) - { - IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); - } } - UE_LOG(LogPS_AI_Behavior, Log, - TEXT("[%s] CoverShootCycle: lost LOS during peek → back to cover"), + 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(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()) { 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(EPS_AI_Behavior_CombatSubState::Advancing)); + SetSubState(BB, EPS_AI_Behavior_CombatSubState::Advancing); - // Release current cover point APS_AI_Behavior_CoverPoint* OldPoint = Cast(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,71 +317,56 @@ 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(EPS_AI_Behavior_CombatSubState::AtCover)); } } else { - // ─── Duck back behind cover ───────────────────── - Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover; - Memory->PhaseDuration = FMath::RandRange(Memory->EffCoverMin, Memory->EffCoverMax); - Memory->Timer = Memory->PhaseDuration; - - BB->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState, - static_cast(EPS_AI_Behavior_CombatSubState::AtCover)); - - // Re-crouch if cover requires it - if (Pawn->Implements()) - { - const APS_AI_Behavior_CoverPoint* CoverPt = - Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); - if (CoverPt && CoverPt->bCrouch) - { - IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); - } - } + // ─── Return to cover ──────────────────────────── + ReturnToCover(OwnerComp, NodeMemory, AIC, Pawn); UE_LOG(LogPS_AI_Behavior, Verbose, - TEXT("[%s] CoverShootCycle: ducking back (cycle %d/%d)"), + 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); + + UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] CoverShootCycle: back at cover, ducking for %.1fs"), + *AIC->GetName(), Memory->PhaseDuration); + } + break; + } + // ─── ADVANCING: Moving to next cover ──────────────────────────── case EPS_AI_Behavior_CombatSubState::Advancing: { 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(EPS_AI_Behavior_CombatSubState::AtCover)); - - // Crouch if the new cover point requires it - if (Pawn->Implements()) - { - const APS_AI_Behavior_CoverPoint* CoverPt = - Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); - if (CoverPt && CoverPt->bCrouch) - { - IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); - } - } + 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,8 +374,303 @@ 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(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(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()) + { + 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(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(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 Result, + UBehaviorTreeComponent* OwnerComp, uint8* NodeMemory) +{ + if (!OwnerComp || !NodeMemory) return; + + FCoverShootMemory* Memory = reinterpret_cast(NodeMemory); + Memory->bEQSRunning = false; + + APS_AI_Behavior_AIController* AIC = Cast(OwnerComp->GetAIOwner()); + if (!AIC || !AIC->GetPawn()) return; + + APawn* Pawn = AIC->GetPawn(); + UBlackboardComponent* BB = OwnerComp->GetBlackboardComponent(); + if (!BB) return; + + AActor* Target = Cast(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()) + { + 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(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()) + { + 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()) { @@ -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(OwnerComp.GetAIOwner()); if (AIC) { - // Stop attacking and stand up APawn* Pawn = AIC->GetPawn(); if (Pawn && Pawn->Implements()) { @@ -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(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)")); } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindCover.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindCover.cpp index c43c91d..0db7c54 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindCover.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindCover.cpp @@ -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(FMath::Lerp(255.0f, 0.0f, Clamped)); + const uint8 G = static_cast(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(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(World), Loc + FVector(0, 0, 30.0f), + 15.0f, 6, ScoreToColor(Score), false, 5.0f); + DrawDebugString(const_cast(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 { diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/EQS/PS_AI_Behavior_EQSContext_CoverLocation.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/EQS/PS_AI_Behavior_EQSContext_CoverLocation.cpp new file mode 100644 index 0000000..98bbfdc --- /dev/null +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/EQS/PS_AI_Behavior_EQSContext_CoverLocation.cpp @@ -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(QueryInstance.Owner.Get()); + if (!QuerierActor) return; + + const APawn* QuerierPawn = Cast(QuerierActor); + if (!QuerierPawn) return; + + APS_AI_Behavior_AIController* AIC = Cast(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); + } +} diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTDecorator_IsCoverNeeded.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTDecorator_IsCoverNeeded.h new file mode 100644 index 0000000..998fc20 --- /dev/null +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTDecorator_IsCoverNeeded.h @@ -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 DangerousTargetTypes; + +protected: + virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override; + virtual FString GetStaticDescription() const override; +}; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_CoverShootCycle.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_CoverShootCycle.h index b4397b6..51edc9b 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_CoverShootCycle.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_CoverShootCycle.h @@ -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 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 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); }; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_FindCover.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_FindCover.h index b2f094b..5d4ea50 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_FindCover.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_FindCover.h @@ -81,6 +81,12 @@ public: UPROPERTY(EditAnywhere, Category = "Cover|EQS Refinement") TObjectPtr 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; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/EQS/PS_AI_Behavior_EQSContext_CoverLocation.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/EQS/PS_AI_Behavior_EQSContext_CoverLocation.h new file mode 100644 index 0000000..04cb0d6 --- /dev/null +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/EQS/PS_AI_Behavior_EQSContext_CoverLocation.h @@ -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; +}; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Definitions.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Definitions.h index 363ff1d..7fb3b71 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Definitions.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Definitions.h @@ -82,10 +82,12 @@ enum class EPS_AI_Behavior_CombatType : uint8 UENUM(BlueprintType) 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"), - Advancing UMETA(DisplayName = "Advancing", ToolTip = "Moving to next cover"), + Engaging UMETA(DisplayName = "Engaging", ToolTip = "Moving to combat position"), + AtCover UMETA(DisplayName = "At Cover", ToolTip = "Ducked behind cover"), + 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"), }; /** Personality trait axes — each scored 0.0 (low) to 1.0 (high). */