From e7c3598dce9d16ed142c2fd70049c406fe33310e Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Fri, 27 Mar 2026 12:43:08 +0100 Subject: [PATCH] Fix combat cycle, spline resume, team identity, and debug system - Add TeamComponent for player pawn team identity (Role-based: Civilian/Enemy/Protector) - Add IsTargetActorValid to interface for dead target filtering - Fix GetTeamAttitudeTowards to check IGenericTeamAgentInterface + TeamComponent - Guarantee BehaviorStopAttack via OnTaskFinished (all exit paths) - Prevent Combat state without ThreatActor (stay Alerted until perception catches up) - Resume spline at closest point from current position after combat - Sync CurrentSpline and SplineProgress to Blackboard in FollowSpline tick - Auto-detect Patrol state when NPC has a spline (fixes Idle speed=0 blocking movement) - Add per-component debug toggles (Personality + SplineFollower independent) - Use AddMovementInput instead of RequestDirectMove for reliable post-combat movement - Add bTickInEditor for Personality debug in Simulate mode Co-Authored-By: Claude Opus 4.6 --- ...AI_Behavior_BTService_EvaluateReaction.cpp | 13 +- .../PS_AI_Behavior_BTService_UpdateThreat.cpp | 5 +- .../BT/PS_AI_Behavior_BTTask_Attack.cpp | 42 ++- ...AI_Behavior_BTTask_FindAndFollowSpline.cpp | 68 ++++ .../BT/PS_AI_Behavior_BTTask_FollowSpline.cpp | 22 ++ .../Private/PS_AI_Behavior_AIController.cpp | 20 +- .../PS_AI_Behavior_PerceptionComponent.cpp | 26 +- .../PS_AI_Behavior_PersonalityComponent.cpp | 124 ++++++- ...PS_AI_Behavior_SplineFollowerComponent.cpp | 304 ++++++++++-------- .../Private/PS_AI_Behavior_SplinePath.cpp | 6 + .../Private/PS_AI_Behavior_TeamComponent.cpp | 19 ++ .../Public/BT/PS_AI_Behavior_BTTask_Attack.h | 1 + .../Public/PS_AI_Behavior_Interface.h | 15 + .../PS_AI_Behavior_PersonalityComponent.h | 14 + .../PS_AI_Behavior_SplineFollowerComponent.h | 10 +- .../Public/PS_AI_Behavior_TeamComponent.h | 46 +++ 16 files changed, 576 insertions(+), 159 deletions(-) create mode 100644 Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_TeamComponent.cpp create mode 100644 Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_TeamComponent.h diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_EvaluateReaction.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_EvaluateReaction.cpp index e017520..90f6087 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_EvaluateReaction.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_EvaluateReaction.cpp @@ -55,7 +55,18 @@ void UPS_AI_Behavior_BTService_EvaluateReaction::TickNode( } // ─── Evaluate and apply the reaction ──────────────────────────────── - const EPS_AI_Behavior_State NewState = Personality->ApplyReaction(); + EPS_AI_Behavior_State NewState = Personality->ApplyReaction(); + + // Don't enter Combat or TakingCover without a valid ThreatActor in BB + if (NewState == EPS_AI_Behavior_State::Combat || NewState == EPS_AI_Behavior_State::TakingCover) + { + const AActor* ThreatActor = Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)); + if (!ThreatActor) + { + // Threat level says fight, but no target yet — stay Alerted until perception catches up + NewState = EPS_AI_Behavior_State::Alerted; + } + } // Write to Blackboard AIC->SetBehaviorState(NewState); diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_UpdateThreat.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_UpdateThreat.cpp index c614486..288eec0 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_UpdateThreat.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_UpdateThreat.cpp @@ -72,11 +72,12 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode( BB->SetValueAsObject(PS_AI_Behavior_BB::ThreatActor, ThreatActor); BB->SetValueAsVector(PS_AI_Behavior_BB::ThreatLocation, ThreatActor->GetActorLocation()); } - else if (FinalThreat <= 0.01f) + else { - // Clear threat data when fully decayed + // No valid threat — clear BB and force threat to zero BB->ClearValue(PS_AI_Behavior_BB::ThreatActor); BB->ClearValue(PS_AI_Behavior_BB::ThreatLocation); + BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f); } // Sync to PersonalityComponent diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_Attack.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_Attack.cpp index 88c7fb2..a5a11d5 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_Attack.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_Attack.cpp @@ -31,6 +31,17 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask( } APawn* Pawn = AIC->GetPawn(); + + // Validate target before starting attack + if (Pawn && Pawn->Implements()) + { + if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(Pawn, Target)) + { + BB->ClearValue(PS_AI_Behavior_BB::ThreatActor); + BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f); + return EBTNodeResult::Failed; + } + } FAttackMemory* Memory = reinterpret_cast(NodeMemory); Memory->bMovingToTarget = false; Memory->bAttacking = false; @@ -72,10 +83,26 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask( AActor* Target = BB ? Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)) : nullptr; if (!Target) { + BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f); FinishLatentTask(OwnerComp, EBTNodeResult::Failed); return; } + // Check if target is still valid (alive, not despawning) via interface + APawn* Pawn = AIC->GetPawn(); + if (Pawn && Pawn->Implements()) + { + if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(Pawn, Target)) + { + // Target is dead/invalid — clear BB, StopAttack called by OnTaskFinished + AIC->StopMovement(); + BB->ClearValue(PS_AI_Behavior_BB::ThreatActor); + BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f); + FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded); + return; + } + } + FAttackMemory* Memory = reinterpret_cast(NodeMemory); // Keep moving toward target if out of range @@ -96,15 +123,26 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask( if (AIC) { AIC->StopMovement(); + } + // StopAttack is called by OnTaskFinished (covers all exit paths) + return EBTNodeResult::Aborted; +} - // Tell the Pawn to stop attacking +void UPS_AI_Behavior_BTTask_Attack::OnTaskFinished( + UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult) +{ + // Always stop attacking when the task ends, regardless of how it ended + APS_AI_Behavior_AIController* AIC = Cast(OwnerComp.GetAIOwner()); + if (AIC) + { APawn* Pawn = AIC->GetPawn(); if (Pawn && Pawn->Implements()) { IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn); } } - return EBTNodeResult::Aborted; + + Super::OnTaskFinished(OwnerComp, NodeMemory, TaskResult); } FString UPS_AI_Behavior_BTTask_Attack::GetStaticDescription() const diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindAndFollowSpline.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindAndFollowSpline.cpp index efeac2c..07ce145 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindAndFollowSpline.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FindAndFollowSpline.cpp @@ -31,6 +31,12 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask( return EBTNodeResult::Failed; } + // Debug: log state on entry + UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] FindAndFollowSpline: bIsFollowing=%d CurrentSpline=%s"), + *AIC->GetName(), + (int32)Follower->bIsFollowing, + Follower->CurrentSpline ? *Follower->CurrentSpline->GetName() : TEXT("null")); + // If already following a spline, don't re-search — just succeed immediately // The Follow Spline task will continue the movement if (Follower->bIsFollowing && Follower->CurrentSpline) @@ -38,6 +44,68 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask( return EBTNodeResult::Succeeded; } + // If paused (e.g. after combat abort) but still has a valid spline, resume it + // Find the closest point on the spline from NPC's CURRENT position (not old pause point) + if (!Follower->bIsFollowing && Follower->CurrentSpline) + { + const FVector PawnLoc = AIC->GetPawn()->GetActorLocation(); + float ClosestDist = 0.0f; + FVector ClosestPoint = FVector::ZeroVector; + const float GapToSpline = Follower->CurrentSpline->GetClosestPointOnSpline( + PawnLoc, ClosestDist, ClosestPoint); + + // Determine direction from pawn forward vs spline tangent at closest point + const FVector PawnFwd = AIC->GetPawn()->GetActorForwardVector(); + const FVector SplineDir = Follower->CurrentSpline->GetWorldDirectionAtDistance(ClosestDist); + const bool bForward = FVector::DotProduct(PawnFwd, SplineDir) >= 0.0f; + + UE_LOG(LogPS_AI_Behavior, Log, + TEXT("[%s] FindAndFollowSpline: resuming spline '%s' at closest point (gap=%.0fcm, dist=%.0f, bWalkToSpline=%d, AcceptanceRadius=%.0f)"), + *AIC->GetName(), *Follower->CurrentSpline->GetName(), GapToSpline, ClosestDist, + (int32)bWalkToSpline, AcceptanceRadius); + + if (bWalkToSpline && GapToSpline > AcceptanceRadius) + { + // Walk to closest spline point via NavMesh + Follower->CurrentDistance = ClosestDist; + const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation( + ClosestPoint, AcceptanceRadius, /*bStopOnOverlap=*/true, + /*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true, + /*bCanStrafe=*/false); + + UE_LOG(LogPS_AI_Behavior, Log, + TEXT("[%s] FindAndFollowSpline: MoveToLocation result=%d (0=Failed, 1=AlreadyAtGoal, 2=RequestSuccessful)"), + *AIC->GetName(), (int32)Result); + + if (Result == EPathFollowingRequestResult::AlreadyAtGoal) + { + Follower->StartFollowingAtDistance(Follower->CurrentSpline, ClosestDist, bForward); + return EBTNodeResult::Succeeded; + } + + if (Result != EPathFollowingRequestResult::Failed) + { + FFindSplineMemory* Memory = reinterpret_cast(NodeMemory); + Memory->bMovingToSpline = true; + return EBTNodeResult::InProgress; + } + // Pathfinding failed — fall through to full re-search below + UE_LOG(LogPS_AI_Behavior, Warning, + TEXT("[%s] FindAndFollowSpline: pathfinding FAILED to reach spline, falling through to re-search"), + *AIC->GetName()); + } + else + { + // Close enough — clear any residual movement request and start following + AIC->StopMovement(); + UE_LOG(LogPS_AI_Behavior, Log, + TEXT("[%s] FindAndFollowSpline: close enough, StartFollowingAtDistance(dist=%.0f, fwd=%d)"), + *AIC->GetName(), ClosestDist, (int32)bForward); + Follower->StartFollowingAtDistance(Follower->CurrentSpline, ClosestDist, bForward); + return EBTNodeResult::Succeeded; + } + } + // Determine NPC type EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Civilian; UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent(); diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FollowSpline.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FollowSpline.cpp index e2403aa..a86f056 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FollowSpline.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_FollowSpline.cpp @@ -5,6 +5,7 @@ #include "PS_AI_Behavior_SplineFollowerComponent.h" #include "PS_AI_Behavior_SplinePath.h" #include "PS_AI_Behavior_Definitions.h" +#include "BehaviorTree/BlackboardComponent.h" UPS_AI_Behavior_BTTask_FollowSpline::UPS_AI_Behavior_BTTask_FollowSpline() { @@ -61,6 +62,19 @@ void UPS_AI_Behavior_BTTask_FollowSpline::TickTask( { UPS_AI_Behavior_SplineFollowerComponent* FollowerCheck = AICCheck->GetPawn()->FindComponentByClass(); + + // Sync spline info to Blackboard + if (FollowerCheck) + { + if (UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent()) + { + BB->SetValueAsObject(PS_AI_Behavior_BB::CurrentSpline, + FollowerCheck->CurrentSpline); + BB->SetValueAsFloat(PS_AI_Behavior_BB::SplineProgress, + FollowerCheck->GetProgress()); + } + } + if (FollowerCheck && !FollowerCheck->bIsFollowing) { FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded); @@ -112,6 +126,14 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FollowSpline::AbortTask( Follower->PauseFollowing(); } } + + // Clear spline info from BB + if (UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent()) + { + BB->ClearValue(PS_AI_Behavior_BB::CurrentSpline); + BB->SetValueAsFloat(PS_AI_Behavior_BB::SplineProgress, 0.0f); + } + return EBTNodeResult::Aborted; } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp index f606bba..94f92a5 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp @@ -4,6 +4,7 @@ #include "PS_AI_Behavior_Interface.h" #include "PS_AI_Behavior_PerceptionComponent.h" #include "PS_AI_Behavior_PersonalityComponent.h" +#include "PS_AI_Behavior_TeamComponent.h" #include "PS_AI_Behavior_PersonalityProfile.h" #include "BehaviorTree/BehaviorTree.h" #include "BehaviorTree/BlackboardComponent.h" @@ -275,12 +276,23 @@ ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const A if (OtherPawn) { - // Check via AIController (NPC with our behavior system) - if (const AAIController* OtherAIC = Cast(OtherPawn->GetController())) + // 1) Check controller's IGenericTeamAgentInterface (AI controllers, custom player controllers) + if (const AController* OtherController = OtherPawn->GetController()) { - OtherTeam = OtherAIC->GetGenericTeamId().GetId(); + if (const IGenericTeamAgentInterface* TeamAgent = Cast(OtherController)) + { + OtherTeam = TeamAgent->GetGenericTeamId().GetId(); + } + } + + // 2) Fallback: check for our TeamComponent on the Pawn (player characters, etc.) + if (OtherTeam == FGenericTeamId::NoTeam) + { + if (const UPS_AI_Behavior_TeamComponent* TeamComp = OtherPawn->FindComponentByClass()) + { + OtherTeam = TeamComp->GetGenericTeamId().GetId(); + } } - // Players or other pawns without AIController → NoTeam (Neutral) } // NoTeam (255) → Neutral diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PerceptionComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PerceptionComponent.cpp index fd5c81e..8bf20ff 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PerceptionComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PerceptionComponent.cpp @@ -180,12 +180,15 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor( AActor* BestThreat = nullptr; float BestScore = -1.0f; + // Get our Pawn for IsTargetActorValid checks + const AAIController* AIC = Cast(Owner); + APawn* MyPawn = AIC ? AIC->GetPawn() : Cast(const_cast(Owner)); + for (AActor* Actor : PerceivedActors) { if (!Actor || Actor == Owner) continue; // Skip self (when owner is AIController, also skip own pawn) - const AAIController* AIC = Cast(Owner); if (AIC && Actor == AIC->GetPawn()) continue; // Skip non-hostile actors (only Hostile actors are valid threats) @@ -198,6 +201,15 @@ AActor* UPS_AI_Behavior_PerceptionComponent::GetHighestThreatActor( } } + // Skip invalid targets (dead, despawning, etc.) via interface + if (MyPawn && MyPawn->Implements()) + { + if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(MyPawn, Actor)) + { + continue; + } + } + // ─── Classify this actor ──────────────────────────────────────── const EPS_AI_Behavior_TargetType ActorType = ClassifyActor(Actor); @@ -268,8 +280,9 @@ float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel() TArray PerceivedActors; GetCurrentlyPerceivedActors(nullptr, PerceivedActors); // All senses - // Get our AIController for attitude checks + // Get our AIController and Pawn for checks const AAIController* AIC = Cast(Owner); + APawn* MyPawn = AIC ? AIC->GetPawn() : Cast(const_cast(Owner)); for (AActor* Actor : PerceivedActors) { @@ -285,6 +298,15 @@ float UPS_AI_Behavior_PerceptionComponent::CalculateThreatLevel() } } + // Skip invalid targets (dead, despawning, etc.) via interface + if (MyPawn && MyPawn->Implements()) + { + if (!IPS_AI_Behavior_Interface::Execute_IsTargetActorValid(MyPawn, Actor)) + { + continue; + } + } + float ActorThreat = 0.0f; const float Dist = FVector::Dist(OwnerLoc, Actor->GetActorLocation()); diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PersonalityComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PersonalityComponent.cpp index 774bb29..23b9442 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PersonalityComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PersonalityComponent.cpp @@ -3,11 +3,19 @@ #include "PS_AI_Behavior_PersonalityComponent.h" #include "PS_AI_Behavior_Interface.h" #include "PS_AI_Behavior_PersonalityProfile.h" +#include "PS_AI_Behavior_AIController.h" +#include "PS_AI_Behavior_SplineFollowerComponent.h" +#include "PS_AI_Behavior_SplinePath.h" +#include "BehaviorTree/BlackboardComponent.h" #include "Net/UnrealNetwork.h" +#include "DrawDebugHelpers.h" +#include "Kismet/KismetSystemLibrary.h" +#include "AIController.h" UPS_AI_Behavior_PersonalityComponent::UPS_AI_Behavior_PersonalityComponent() { - PrimaryComponentTick.bCanEverTick = false; + PrimaryComponentTick.bCanEverTick = true; + bTickInEditor = true; // Required for Simulate In Editor SetIsReplicatedByDefault(true); } @@ -101,10 +109,19 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c return EPS_AI_Behavior_State::Alerted; } - // No threat — maintain patrol or idle - return (CurrentState == EPS_AI_Behavior_State::Patrol) - ? EPS_AI_Behavior_State::Patrol - : EPS_AI_Behavior_State::Idle; + // No threat — patrol if following a spline, otherwise idle + if (const AActor* Owner = GetOwner()) + { + if (const UPS_AI_Behavior_SplineFollowerComponent* Spline = + Owner->FindComponentByClass()) + { + if (Spline->bIsFollowing || Spline->CurrentSpline) + { + return EPS_AI_Behavior_State::Patrol; + } + } + } + return EPS_AI_Behavior_State::Idle; } EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::ApplyReaction() @@ -175,6 +192,103 @@ void UPS_AI_Behavior_PersonalityComponent::HandleStateChanged( } } +void UPS_AI_Behavior_PersonalityComponent::TickComponent( + float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + if (bDebug) + { + DrawDebugInfo(); + } +} + +void UPS_AI_Behavior_PersonalityComponent::DrawDebugInfo() const +{ + const AActor* Owner = GetOwner(); + if (!Owner) return; + + const UWorld* World = GetWorld(); + if (!World) return; + + // Position: above the NPC's head + const FVector HeadPos = Owner->GetActorLocation() + FVector(0.f, 0.f, 120.f); + + // ─── Gather info ──────────────────────────────────────────────────── + const FString NPCName = Owner->GetName(); + const EPS_AI_Behavior_NPCType NPCType = GetNPCType(); + + // TeamId + Hostile from AIController + uint8 DebugTeamId = 255; + FString ThreatActorName = TEXT("none"); + float ThreatLevel = PerceivedThreatLevel; + bool bHostile = false; + + const APawn* Pawn = Cast(Owner); + if (Pawn) + { + if (const APS_AI_Behavior_AIController* AIC = Cast(Pawn->GetController())) + { + DebugTeamId = AIC->GetGenericTeamId().GetId(); + + // Get ThreatActor from Blackboard + if (const UBlackboardComponent* BB = AIC->GetBlackboardComponent()) + { + if (AActor* ThreatActor = Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor))) + { + ThreatActorName = ThreatActor->GetName(); + } + } + } + + if (Pawn->Implements()) + { + bHostile = IPS_AI_Behavior_Interface::Execute_IsBehaviorHostile(const_cast(Pawn)); + } + } + + // Spline info + FString SplineInfo = TEXT(""); + if (const UPS_AI_Behavior_SplineFollowerComponent* Spline = + Owner->FindComponentByClass()) + { + if (Spline->bIsFollowing && Spline->CurrentSpline) + { + SplineInfo = FString::Printf(TEXT("\nSpline: %s (%.0f%%)"), + *Spline->CurrentSpline->GetName(), Spline->GetProgress() * 100.f); + } + } + + // ─── Color by state ───────────────────────────────────────────────── + FColor TextColor; + switch (CurrentState) + { + case EPS_AI_Behavior_State::Idle: TextColor = FColor::White; break; + case EPS_AI_Behavior_State::Patrol: TextColor = FColor::Green; break; + case EPS_AI_Behavior_State::Alerted: TextColor = FColor::Yellow; break; + case EPS_AI_Behavior_State::Combat: TextColor = FColor::Red; break; + case EPS_AI_Behavior_State::Fleeing: TextColor = FColor::Orange; break; + case EPS_AI_Behavior_State::TakingCover: TextColor = FColor::Cyan; break; + case EPS_AI_Behavior_State::Dead: TextColor = FColor(80, 80, 80); break; + default: TextColor = FColor::White; break; + } + + // ─── Build text ───────────────────────────────────────────────────── + const FString DebugText = FString::Printf( + TEXT("%s [%s] Team:%d %s\n%s Threat:%.2f → %s%s"), + *NPCName, + *UEnum::GetDisplayValueAsText(NPCType).ToString(), + DebugTeamId, + bHostile ? TEXT("HOSTILE") : TEXT(""), + *UEnum::GetDisplayValueAsText(CurrentState).ToString(), + ThreatLevel, + *ThreatActorName, + *SplineInfo); + + UKismetSystemLibrary::DrawDebugString( + GetOwner(), HeadPos, DebugText, nullptr, FLinearColor(TextColor), 0.f); +} + EPS_AI_Behavior_NPCType UPS_AI_Behavior_PersonalityComponent::GetNPCType() const { // Prefer the IPS_AI_Behavior interface on the owning actor diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_SplineFollowerComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_SplineFollowerComponent.cpp index 96b6248..03e8771 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_SplineFollowerComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_SplineFollowerComponent.cpp @@ -154,141 +154,158 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent( return; } - // ─── Advance along spline ─────────────────────────────────────────── - // Use the NPC's actual movement speed (from CMC) instead of a fixed speed. - // This keeps the target point in sync with how fast the character really moves. - float Speed = GetEffectiveSpeed(); - ACharacter* OwnerCharacter = Cast(GetOwner()); - if (OwnerCharacter && OwnerCharacter->GetCharacterMovement()) - { - Speed = OwnerCharacter->GetCharacterMovement()->GetMaxSpeed(); - } - const float Delta = Speed * DeltaTime; - const float PrevDistance = CurrentDistance; - const bool bPrevForward = bMovingForward; - - if (bMovingForward) - { - CurrentDistance += Delta; - } - else - { - CurrentDistance -= Delta; - } - - // Log direction changes and significant distance jumps - if (bDrawDebug) - { - if (bMovingForward != bPrevForward) - { - UE_LOG(LogPS_AI_Behavior, Warning, - TEXT("[%s] DIRECTION CHANGED: %s -> %s at dist=%.0f/%.0f"), - GetOwner() ? *GetOwner()->GetName() : TEXT("?"), - bPrevForward ? TEXT("FWD") : TEXT("BWD"), - bMovingForward ? TEXT("FWD") : TEXT("BWD"), - CurrentDistance, SplineLen); - } - - const float DistJump = FMath::Abs(CurrentDistance - PrevDistance); - if (DistJump > Delta * 2.0f && DistJump > 10.0f) - { - UE_LOG(LogPS_AI_Behavior, Warning, - TEXT("[%s] DIST JUMP: %.0f -> %.0f (delta=%.1f, expected=%.1f) fwd=%d"), - GetOwner() ? *GetOwner()->GetName() : TEXT("?"), - PrevDistance, CurrentDistance, DistJump, Delta, - (int32)bMovingForward); - } - } - - // ─── End of spline handling ───────────────────────────────────────── - if (CurrentDistance >= SplineLen) - { - if (CurrentSpline->SplineComp && CurrentSpline->SplineComp->IsClosedLoop()) - { - CurrentDistance = FMath::Fmod(CurrentDistance, SplineLen); - } - else if (bReverseAtEnd) - { - // Clamp to the end — don't reverse the target point yet. - // Wait until the NPC is close enough, then reverse. - CurrentDistance = SplineLen; - const FVector EndPoint = CurrentSpline->GetWorldLocationAtDistance(SplineLen); - const float GapToEnd = FVector::Dist2D(GetOwner()->GetActorLocation(), EndPoint); - if (GapToEnd < 80.0f) - { - // NPC has caught up — now reverse - bMovingForward = false; - } - // Otherwise, target stays at the end and NPC walks toward it - } - else - { - CurrentDistance = SplineLen; - bIsFollowing = false; - OnSplineEndReached.Broadcast(CurrentSpline); - return; - } - } - else if (CurrentDistance <= 0.0f) - { - if (CurrentSpline->SplineComp && CurrentSpline->SplineComp->IsClosedLoop()) - { - CurrentDistance = SplineLen + CurrentDistance; - } - else if (bReverseAtEnd) - { - // Clamp to the start — wait for NPC to catch up - CurrentDistance = 0.0f; - const FVector StartPoint = CurrentSpline->GetWorldLocationAtDistance(0.0f); - const float GapToStart = FVector::Dist2D(GetOwner()->GetActorLocation(), StartPoint); - if (GapToStart < 80.0f) - { - // NPC has caught up — now reverse - bMovingForward = true; - } - } - else - { - CurrentDistance = 0.0f; - bIsFollowing = false; - OnSplineEndReached.Broadcast(CurrentSpline); - return; - } - } - - // ─── Move the pawn ────────────────────────────────────────────────── - const FVector TargetLocation = CurrentSpline->GetWorldLocationAtDistance(CurrentDistance); - const FRotator TargetRotation = CurrentSpline->GetWorldRotationAtDistance(CurrentDistance); + // ─── Pure Pursuit: project NPC onto spline, target = lookahead ahead ── + // This eliminates gap accumulation in turns. The NPC always steers toward + // a point that follows the curve naturally at a fixed distance ahead. AActor* Owner = GetOwner(); const FVector CurrentLocation = Owner->GetActorLocation(); const FRotator CurrentRotation = Owner->GetActorRotation(); - // Use Character movement if available for proper physics/collision - ACharacter* Character = Cast(Owner); - if (Character && Character->GetCharacterMovement()) - { - // Steer toward the spline point directly. - // The spline advances smoothly each frame so the target point moves along the curve. - // This gives natural cornering without drifting. - const FVector ToTarget = TargetLocation - CurrentLocation; - const float GapToSpline = ToTarget.Size2D(); + // Project NPC position onto the spline to find where we actually are + float ProjectedDistance = 0.0f; + FVector ProjectedPoint; + CurrentSpline->GetClosestPointOnSpline(CurrentLocation, ProjectedDistance, ProjectedPoint); - FVector DesiredVelocity; - if (GapToSpline < 1.0f) + // Ensure monotonic progress: don't let projection jump backward past current + // (can happen at tight S-curves where two segments are close in world space) + const float ProgressDir = bMovingForward ? 1.0f : -1.0f; + const float ProgressDelta = (ProjectedDistance - CurrentDistance) * ProgressDir; + if (ProgressDelta < -LookaheadDistance) + { + // Projection jumped too far backward — keep current distance and advance normally + float Speed = GetEffectiveSpeed(); + ACharacter* TmpChar = Cast(Owner); + if (TmpChar && TmpChar->GetCharacterMovement()) { - // Exactly on the spline — use tangent to avoid zero velocity - const FVector SplineTangent = CurrentSpline->GetWorldDirectionAtDistance(CurrentDistance) - * (bMovingForward ? 1.0f : -1.0f); - DesiredVelocity = SplineTangent * Speed; + Speed = TmpChar->GetCharacterMovement()->GetMaxSpeed(); + } + if (bMovingForward) + { + CurrentDistance += Speed * DeltaTime; } else { - // Steer toward the target point on the spline, clamped to Speed - DesiredVelocity = ToTarget.GetSafeNormal() * Speed; + CurrentDistance -= Speed * DeltaTime; + } + } + else + { + CurrentDistance = ProjectedDistance; + } + + // ─── Compute target (lookahead) distance ──────────────────────────── + float TargetDistance; + if (bMovingForward) + { + TargetDistance = CurrentDistance + LookaheadDistance; + } + else + { + TargetDistance = CurrentDistance - LookaheadDistance; + } + + // ─── End of spline handling ───────────────────────────────────────── + if (TargetDistance >= SplineLen) + { + if (CurrentSpline->SplineComp && CurrentSpline->SplineComp->IsClosedLoop()) + { + TargetDistance = FMath::Fmod(TargetDistance, SplineLen); + } + else if (bReverseAtEnd) + { + // Clamp target to end, and reverse when NPC is close enough + TargetDistance = SplineLen; + const FVector EndPoint = CurrentSpline->GetWorldLocationAtDistance(SplineLen); + const float GapToEnd = FVector::Dist2D(CurrentLocation, EndPoint); + if (GapToEnd < 80.0f) + { + bMovingForward = false; + TargetDistance = CurrentDistance - LookaheadDistance; + TargetDistance = FMath::Max(TargetDistance, 0.0f); + } + } + else + { + TargetDistance = SplineLen; + const FVector EndPoint = CurrentSpline->GetWorldLocationAtDistance(SplineLen); + const float GapToEnd = FVector::Dist2D(CurrentLocation, EndPoint); + if (GapToEnd < 80.0f) + { + bIsFollowing = false; + OnSplineEndReached.Broadcast(CurrentSpline); + return; + } + } + } + else if (TargetDistance <= 0.0f) + { + if (CurrentSpline->SplineComp && CurrentSpline->SplineComp->IsClosedLoop()) + { + TargetDistance = SplineLen + TargetDistance; + } + else if (bReverseAtEnd) + { + TargetDistance = 0.0f; + const FVector StartPoint = CurrentSpline->GetWorldLocationAtDistance(0.0f); + const float GapToStart = FVector::Dist2D(CurrentLocation, StartPoint); + if (GapToStart < 80.0f) + { + bMovingForward = true; + TargetDistance = CurrentDistance + LookaheadDistance; + TargetDistance = FMath::Min(TargetDistance, SplineLen); + } + } + else + { + TargetDistance = 0.0f; + const FVector StartPoint = CurrentSpline->GetWorldLocationAtDistance(0.0f); + const float GapToStart = FVector::Dist2D(CurrentLocation, StartPoint); + if (GapToStart < 80.0f) + { + bIsFollowing = false; + OnSplineEndReached.Broadcast(CurrentSpline); + return; + } + } + } + + // ─── Move the pawn ────────────────────────────────────────────────── + const FVector TargetLocation = CurrentSpline->GetWorldLocationAtDistance(TargetDistance); + + float Speed = GetEffectiveSpeed(); + ACharacter* Character = Cast(Owner); + if (Character && Character->GetCharacterMovement()) + { + Speed = Character->GetCharacterMovement()->GetMaxSpeed(); + + const FVector ToTarget = TargetLocation - CurrentLocation; + const float GapToTarget = ToTarget.Size2D(); + + FVector MoveDirection; + if (GapToTarget < 1.0f) + { + // On top of the target — use spline tangent direction + MoveDirection = CurrentSpline->GetWorldDirectionAtDistance(CurrentDistance) + * (bMovingForward ? 1.0f : -1.0f); + } + else + { + MoveDirection = ToTarget.GetSafeNormal(); } - Character->GetCharacterMovement()->RequestDirectMove(DesiredVelocity, /*bForceMaxSpeed=*/false); + // Use AddMovementInput — works reliably regardless of AIController movement state + Character->AddMovementInput(MoveDirection, 1.0f); + + // Debug: log velocity to verify movement is happening + const FVector Vel = Character->GetVelocity(); + if (Vel.Size() < 1.0f) + { + UE_LOG(LogPS_AI_Behavior, Warning, + TEXT("[%s] SplineFollower: AddMovementInput but velocity=%.1f! MaxSpeed=%.0f Gap=%.0f CMC_MovementMode=%d"), + *GetOwner()->GetName(), Vel.Size(), Speed, GapToTarget, + (int32)Character->GetCharacterMovement()->MovementMode.GetValue()); + } } else { @@ -296,8 +313,7 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent( Owner->SetActorLocation(TargetLocation); } - // Smooth rotation — face toward the target point (not the spline tangent) - // This prevents the NPC from looking too far into corners + // Smooth rotation — face toward the target point (follows curve naturally) const FVector ToTargetDir = (TargetLocation - CurrentLocation).GetSafeNormal2D(); FRotator FinalTargetRot; if (!ToTargetDir.IsNearlyZero()) @@ -306,8 +322,8 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent( } else { - // Fallback to spline tangent if on top of the target - FinalTargetRot = TargetRotation; + // Fallback to spline tangent + FinalTargetRot = CurrentSpline->GetWorldRotationAtDistance(CurrentDistance); if (!bMovingForward) { FinalTargetRot.Yaw += 180.0f; @@ -316,32 +332,36 @@ void UPS_AI_Behavior_SplineFollowerComponent::TickComponent( const FRotator SmoothedRot = FMath::RInterpConstantTo( CurrentRotation, FinalTargetRot, DeltaTime, RotationInterpSpeed); - Owner->SetActorRotation(FRotator(0.0f, SmoothedRot.Yaw, 0.0f)); // Only yaw + Owner->SetActorRotation(FRotator(0.0f, SmoothedRot.Yaw, 0.0f)); // ─── Debug drawing ───────────────────────────────────────────────── - if (bDrawDebug) + if (bDebug) { const UWorld* World = GetWorld(); if (World) { - // Target point on spline (green sphere) + // Projected point on spline (small blue sphere = where NPC actually is on spline) + DrawDebugSphere(World, ProjectedPoint, 10.0f, 6, FColor::Blue, false, -1.0f, 0, 1.5f); + + // Lookahead target point (green sphere = where NPC steers toward) DrawDebugSphere(World, TargetLocation, 15.0f, 8, FColor::Green, false, -1.0f, 0, 2.0f); - // Line from NPC to target point (yellow = gap) + // Line from NPC to lookahead target (yellow) DrawDebugLine(World, CurrentLocation, TargetLocation, FColor::Yellow, false, -1.0f, 0, 1.5f); - // Spline tangent direction (cyan arrow) + // Spline tangent direction at NPC position (cyan arrow) const FVector Tangent = CurrentSpline->GetWorldDirectionAtDistance(CurrentDistance) * (bMovingForward ? 1.0f : -1.0f); - DrawDebugDirectionalArrow(World, TargetLocation, TargetLocation + Tangent * 150.0f, + DrawDebugDirectionalArrow(World, ProjectedPoint, ProjectedPoint + Tangent * 150.0f, 20.0f, FColor::Cyan, false, -1.0f, 0, 2.0f); - // Gap distance text - const float DebugGap = FVector::Dist(CurrentLocation, TargetLocation); + // Info text + const float DebugGap = FVector::Dist2D(CurrentLocation, TargetLocation); + const float SplineGap = FVector::Dist2D(CurrentLocation, ProjectedPoint); DrawDebugString(World, CurrentLocation + FVector(0, 0, 100.0f), - FString::Printf(TEXT("Gap: %.0fcm Dist: %.0f/%.0f"), - DebugGap, CurrentDistance, CurrentSpline->GetSplineLength()), - nullptr, FColor::White, 0.0f, true); + FString::Printf(TEXT("Look: %.0fcm Off: %.0fcm D: %.0f/%.0f"), + DebugGap, SplineGap, CurrentDistance, SplineLen), + nullptr, FColor::White, -1.0f, true); } } @@ -382,7 +402,7 @@ void UPS_AI_Behavior_SplineFollowerComponent::HandleJunctions() LastHandledJunctionIndex = i; OnApproachingJunction.Broadcast(CurrentSpline, i, DistToJunction); - if (bDrawDebug) + if (bDebug) { UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Junction detected: idx=%d, dist along=%.0f, dist to=%.0fcm, worldPos=(%.0f,%.0f,%.0f)"), @@ -398,7 +418,7 @@ void UPS_AI_Behavior_SplineFollowerComponent::HandleJunctions() const float Roll = FMath::FRand(); if (Roll > SwitchChance) { - if (bDrawDebug) + if (bDebug) { UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Junction %d: skipped (roll=%.2f > chance=%.2f)"), @@ -435,7 +455,7 @@ void UPS_AI_Behavior_SplineFollowerComponent::HandleJunctions() const float WorldGap = FVector::Dist(CurrentPos, JunctionPos); if (WorldGap > JunctionDetectionDistance * 2.0f) { - if (bDrawDebug) + if (bDebug) { UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Junction %d: rejected (worldGap=%.0f > detect=%.0f)"), @@ -474,7 +494,7 @@ void UPS_AI_Behavior_SplineFollowerComponent::HandleJunctions() // This prevents full U-turns but allows wide turns at crossings if (BestDot < -0.7f) { - if (bDrawDebug) + if (bDebug) { UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Junction %d: rejected U-turn (dot=%.2f)"), diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_SplinePath.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_SplinePath.cpp index f8d9a9b..d11ea0e 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_SplinePath.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_SplinePath.cpp @@ -19,6 +19,12 @@ void APS_AI_Behavior_SplinePath::BeginPlay() { Super::BeginPlay(); UpdateSplineVisualization(); + + // Hide spline debug at runtime — only visible in editor + if (SplineComp) + { + SplineComp->SetDrawDebug(false); + } } bool APS_AI_Behavior_SplinePath::IsAccessibleTo(EPS_AI_Behavior_NPCType NPCType) const diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_TeamComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_TeamComponent.cpp new file mode 100644 index 0000000..996fa01 --- /dev/null +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_TeamComponent.cpp @@ -0,0 +1,19 @@ +// Copyright Asterion. All Rights Reserved. + +#include "PS_AI_Behavior_TeamComponent.h" + +UPS_AI_Behavior_TeamComponent::UPS_AI_Behavior_TeamComponent() +{ + PrimaryComponentTick.bCanEverTick = false; +} + +uint8 UPS_AI_Behavior_TeamComponent::GetTeamId() const +{ + switch (Role) + { + case EPS_AI_Behavior_NPCType::Civilian: return 1; + case EPS_AI_Behavior_NPCType::Enemy: return 2; + case EPS_AI_Behavior_NPCType::Protector: return 3; + default: return 1; + } +} diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_Attack.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_Attack.h index f770921..a89ea24 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_Attack.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_Attack.h @@ -32,6 +32,7 @@ protected: virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override; virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; + virtual void OnTaskFinished(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult) override; virtual FString GetStaticDescription() const override; private: diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h index ada564c..69041c4 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h @@ -101,6 +101,21 @@ public: UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior") void OnBehaviorStateChanged(EPS_AI_Behavior_State NewState, EPS_AI_Behavior_State OldState); + // ─── Target Validation ────────────────────────────────────────────── + + /** + * Check whether a perceived actor is still a valid target (alive, not despawning, etc.). + * Called by the perception and combat systems to discard dead or invalid targets. + * + * The host project implements this to check its own health/death system. + * Default returns true (all actors are valid unless the Pawn says otherwise). + * + * @param TargetActor The actor to validate. + * @return True if the actor can still be targeted. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior") + bool IsTargetActorValid(AActor* TargetActor) const; + // ─── Combat ───────────────────────────────────────────────────────── /** diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_PersonalityComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_PersonalityComponent.h index 4ea8872..4eb4a82 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_PersonalityComponent.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_PersonalityComponent.h @@ -36,6 +36,15 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality") TObjectPtr Profile; + /** + * Master debug toggle for this NPC. + * When enabled, draws floating text above the NPC's head with: + * Name, NPCType, TeamId, State, ThreatLevel, ThreatActor, Hostile, Spline. + * Also enables debug visuals on SplineFollowerComponent. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Personality|Debug") + bool bDebug = false; + // ─── Runtime State ────────────────────────────────────────────────── /** @@ -106,6 +115,8 @@ public: protected: virtual void BeginPlay() override; + virtual void TickComponent(float DeltaTime, ELevelTick TickType, + FActorComponentTickFunction* ThisTickFunction) override; UFUNCTION() void OnRep_CurrentState(EPS_AI_Behavior_State OldState); @@ -118,4 +129,7 @@ private: * - Calls IPS_AI_Behavior::OnBehaviorStateChanged on the Pawn */ void HandleStateChanged(EPS_AI_Behavior_State OldState, EPS_AI_Behavior_State NewState); + + /** Draw floating debug text above the NPC's head. */ + void DrawDebugInfo() const; }; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_SplineFollowerComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_SplineFollowerComponent.h index 30b587a..60754da 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_SplineFollowerComponent.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_SplineFollowerComponent.h @@ -58,6 +58,14 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower", meta = (ClampMin = "30.0")) float RotationInterpSpeed = 360.0f; + /** + * How far ahead on the spline to place the target point (cm). + * Larger = smoother wider turns, smaller = tighter cornering. + * The NPC always steers toward this point, which follows the spline curve naturally. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower", meta = (ClampMin = "50.0")) + float LookaheadDistance = 200.0f; + /** * Whether to auto-choose a spline at junctions. * If false, OnApproachingJunction fires and you must call SwitchToSpline manually. @@ -75,7 +83,7 @@ public: /** Draw debug info: target point on spline, direction, gap distance. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spline Follower|Debug") - bool bDrawDebug = false; + bool bDebug = false; // ─── Runtime State ────────────────────────────────────────────────── diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_TeamComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_TeamComponent.h new file mode 100644 index 0000000..37a35c8 --- /dev/null +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_TeamComponent.h @@ -0,0 +1,46 @@ +// Copyright Asterion. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "GenericTeamAgentInterface.h" +#include "PS_AI_Behavior_Definitions.h" +#include "PS_AI_Behavior_TeamComponent.generated.h" + +/** + * Drop this component on any Pawn (player or non-AI character) to give it + * a role that the PS AI Behavior perception system will recognize. + * + * NPCs controlled by PS_AI_Behavior_AIController already have their role + * set automatically via the Interface — this component is meant for Pawns + * WITHOUT that controller (typically the player character). + * + * Role determines team affiliation: + * Civilian (Team 1) — Enemies attack, Protectors defend + * Enemy (Team 2) — Protectors and Civilians react + * Protector (Team 3) — Allied with Civilians, hostile to Enemies + */ +UCLASS(ClassGroup = "PS AI Behavior", meta = (BlueprintSpawnableComponent, + DisplayName = "PS AI Behavior Pawn Identity")) +class PS_AI_BEHAVIOR_API UPS_AI_Behavior_TeamComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UPS_AI_Behavior_TeamComponent(); + + /** Role of this actor in the behavior system. Determines team affiliation. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PS AI Behavior|Team") + EPS_AI_Behavior_NPCType Role = EPS_AI_Behavior_NPCType::Civilian; + + /** Change role at runtime. */ + UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Team") + void SetRole(EPS_AI_Behavior_NPCType NewRole) { Role = NewRole; } + + /** Returns the TeamId derived from the current Role. */ + uint8 GetTeamId() const; + + /** Returns the TeamId as FGenericTeamId. */ + FGenericTeamId GetGenericTeamId() const { return FGenericTeamId(GetTeamId()); } +};