Compare commits

..

No commits in common. "d471714fbd65e6d5d2a49b4567860b8723e55108" and "69b9844a4bc766a4daf2cc4b275cbc845a8ed0df" have entirely different histories.

31 changed files with 82 additions and 208 deletions

View File

@ -4,7 +4,6 @@
#include "PS_AI_Behavior_AIController.h" #include "PS_AI_Behavior_AIController.h"
#include "PS_AI_Behavior_PerceptionComponent.h" #include "PS_AI_Behavior_PerceptionComponent.h"
#include "PS_AI_Behavior_PersonalityComponent.h" #include "PS_AI_Behavior_PersonalityComponent.h"
#include "PS_AI_Behavior_PersonalityProfile.h"
#include "PS_AI_Behavior_Statics.h" #include "PS_AI_Behavior_Statics.h"
#include "PS_AI_Behavior_Settings.h" #include "PS_AI_Behavior_Settings.h"
#include "PS_AI_Behavior_Definitions.h" #include "PS_AI_Behavior_Definitions.h"
@ -35,8 +34,6 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
return; return;
} }
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
// Debug: check what perception sees // Debug: check what perception sees
TArray<AActor*> PerceivedActors; TArray<AActor*> PerceivedActors;
Perception->GetCurrentlyPerceivedActors(nullptr, PerceivedActors); Perception->GetCurrentlyPerceivedActors(nullptr, PerceivedActors);
@ -63,13 +60,8 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
const float StoredThreat = BB->GetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel); const float StoredThreat = BB->GetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel);
// Apply decay when no threat, or take the max of new vs decayed // Apply decay when no threat, or take the max of new vs decayed
// Decay rate comes from PersonalityProfile (per-archetype), fallback to global Settings const UPS_AI_Behavior_Settings* Settings = GetDefault<UPS_AI_Behavior_Settings>();
float DecayRate = GetDefault<UPS_AI_Behavior_Settings>()->ThreatDecayRate; const float DecayedThreat = FMath::Max(0.0f, StoredThreat - Settings->ThreatDecayRate * DeltaSeconds);
if (Personality && Personality->Profile)
{
DecayRate = Personality->Profile->ThreatDecayRate;
}
const float DecayedThreat = FMath::Max(0.0f, StoredThreat - DecayRate * DeltaSeconds);
const float FinalThreat = FMath::Max(RawThreat, DecayedThreat); const float FinalThreat = FMath::Max(RawThreat, DecayedThreat);
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, FinalThreat); BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, FinalThreat);
@ -269,6 +261,7 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
} }
// Sync to PersonalityComponent // Sync to PersonalityComponent
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
if (Personality) if (Personality)
{ {
Personality->PerceivedThreatLevel = FinalThreat; Personality->PerceivedThreatLevel = FinalThreat;

View File

@ -103,19 +103,17 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
const float ApproachRange = (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee) const float ApproachRange = (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee)
? Memory->MinRange * 0.5f : (Memory->MinRange + Memory->MaxRange) * 0.5f; ? Memory->MinRange * 0.5f : (Memory->MinRange + Memory->MaxRange) * 0.5f;
// Start attack immediately (draw weapon, enter combat stance)
// regardless of range or LOS — the NPC is committed to combat
if (Pawn->Implements<UPS_AI_Behavior_Interface>())
{
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
Memory->bAttacking = true;
}
// Check if already in range // Check if already in range
const float DistToTarget = FVector::Dist(Pawn->GetActorLocation(), Target->GetActorLocation()); const float DistToTarget = FVector::Dist(Pawn->GetActorLocation(), Target->GetActorLocation());
if (DistToTarget <= Memory->MaxRange) if (DistToTarget <= Memory->MaxRange)
{ {
Memory->bInRange = true; Memory->bInRange = true;
// Only start attacking if we have LOS (ranged) or always (melee)
if (Memory->bHasLOS && Pawn->Implements<UPS_AI_Behavior_Interface>())
{
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
Memory->bAttacking = true;
}
} }
// Initial move toward target if not in range // Initial move toward target if not in range

View File

@ -132,13 +132,6 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::ExecuteTask(
// Face the threat while in cover // Face the threat while in cover
AIC->SetFocus(Target); AIC->SetFocus(Target);
// Start attack immediately (draw weapon, enter combat stance)
APawn* Pawn = AIC->GetPawn();
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
{
IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target);
}
if (Result == EPathFollowingRequestResult::AlreadyAtGoal) if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
{ {
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover; Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;

View File

@ -148,64 +148,34 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
if (bWalkToSpline && GapToSpline > AcceptanceRadius) if (bWalkToSpline && GapToSpline > AcceptanceRadius)
{ {
// Try to reach the closest spline point. If blocked, sample other points along the spline. // Walk to spline first via NavMesh
const float SplineLength = ClosestSpline->GetSplineLength(); const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
const int32 NumSamples = 8; SplinePoint, AcceptanceRadius, /*bStopOnOverlap=*/true,
const float SampleStep = SplineLength / NumSamples; /*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true,
/*bCanStrafe=*/false);
// Start with the closest point, then try evenly spaced samples if (Result == EPathFollowingRequestResult::Failed)
TArray<float> DistancesToTry;
DistancesToTry.Add(DistAlongSpline);
for (int32 i = 1; i <= NumSamples; ++i)
{ {
const float D = FMath::Fmod(DistAlongSpline + i * SampleStep, SplineLength); // Can't reach via NavMesh — try starting anyway (snap)
DistancesToTry.Add(D); const FVector Fwd = AIC->GetPawn()->GetActorForwardVector();
const FVector SpDir = ClosestSpline->GetWorldDirectionAtDistance(DistAlongSpline);
Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline,
FVector::DotProduct(Fwd, SpDir) >= 0.0f);
return EBTNodeResult::Succeeded;
} }
float ReachableDist = -1.0f; if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
FVector ReachablePoint = FVector::ZeroVector;
for (float TestDist : DistancesToTry)
{ {
const FVector TestPoint = ClosestSpline->GetWorldLocationAtDistance(TestDist); const FVector Fwd = AIC->GetPawn()->GetActorForwardVector();
const EPathFollowingRequestResult::Type TestResult = AIC->MoveToLocation( const FVector SpDir = ClosestSpline->GetWorldDirectionAtDistance(DistAlongSpline);
TestPoint, AcceptanceRadius, true, true, true, false); Follower->StartFollowingAtDistance(ClosestSpline, DistAlongSpline,
FVector::DotProduct(Fwd, SpDir) >= 0.0f);
if (TestResult == EPathFollowingRequestResult::AlreadyAtGoal) return EBTNodeResult::Succeeded;
{
const FVector Fwd = AIC->GetPawn()->GetActorForwardVector();
const FVector SpDir = ClosestSpline->GetWorldDirectionAtDistance(TestDist);
Follower->StartFollowingAtDistance(ClosestSpline, TestDist,
FVector::DotProduct(Fwd, SpDir) >= 0.0f);
return EBTNodeResult::Succeeded;
}
if (TestResult != EPathFollowingRequestResult::Failed)
{
ReachableDist = TestDist;
ReachablePoint = TestPoint;
break; // Found a reachable point, use it
}
// This point is blocked, try next
AIC->StopMovement();
} }
if (ReachableDist < 0.0f) // Store the spline to connect to after reaching it
{
UE_LOG(LogPS_AI_Behavior, Log,
TEXT("[%s] FindAndFollowSpline: no reachable point on spline '%s' (%d samples tested)"),
*AIC->GetName(), *ClosestSpline->GetName(), DistancesToTry.Num());
return EBTNodeResult::Failed;
}
UE_LOG(LogPS_AI_Behavior, Log,
TEXT("[%s] FindAndFollowSpline: walking to reachable spline point at dist=%.0f (gap=%.0fcm)"),
*AIC->GetName(), ReachableDist,
FVector::Dist(AIC->GetPawn()->GetActorLocation(), ReachablePoint));
Follower->CurrentSpline = ClosestSpline; Follower->CurrentSpline = ClosestSpline;
Follower->CurrentDistance = ReachableDist; Follower->CurrentDistance = DistAlongSpline;
FFindSplineMemory* Memory = reinterpret_cast<FFindSplineMemory*>(NodeMemory); FFindSplineMemory* Memory = reinterpret_cast<FFindSplineMemory*>(NodeMemory);
Memory->bMovingToSpline = true; Memory->bMovingToSpline = true;

View File

@ -207,51 +207,21 @@ void UPS_AI_Behavior_BTTask_FindCover::TickTask(
if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle) if (AIC->GetMoveStatus() == EPathFollowingStatus::Idle)
{ {
// Verify we actually reached the cover — pathfinding can stop early (blocked by other NPCs, etc.)
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
APawn* Pawn = AIC->GetPawn();
if (BB && Pawn)
{
const FVector CoverLoc = BB->GetValueAsVector(PS_AI_Behavior_BB::CoverLocation);
const float DistToCover = FVector::Dist2D(Pawn->GetActorLocation(), CoverLoc);
UE_LOG(LogPS_AI_Behavior, Log,
TEXT("[%s] FindCover: MoveStatus=Idle, dist2D=%.0fcm, acceptance=%.0fcm, coverLoc=%s, pawnLoc=%s"),
*AIC->GetName(), DistToCover, AcceptanceRadius,
*CoverLoc.ToString(), *Pawn->GetActorLocation().ToString());
if (DistToCover > AcceptanceRadius * 2.0f)
{
// Still too far — retry movement
const EPathFollowingRequestResult::Type Retry = AIC->MoveToLocation(
CoverLoc, AcceptanceRadius, true, true, true, false);
if (Retry == EPathFollowingRequestResult::Failed)
{
// Can't reach at all — give up
Memory->bMoveRequested = false;
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
return;
}
if (Retry != EPathFollowingRequestResult::AlreadyAtGoal)
{
return; // Keep waiting for retry movement
}
// AlreadyAtGoal — fall through to success
}
}
Memory->bMoveRequested = false; Memory->bMoveRequested = false;
// Crouch at cover if the point requires it // Crouch at cover if the point requires it
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>() && BB) APawn* Pawn = AIC->GetPawn();
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
{ {
const APS_AI_Behavior_CoverPoint* CoverPt = UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint)); if (BB)
if (CoverPt && CoverPt->bCrouch)
{ {
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true); const APS_AI_Behavior_CoverPoint* CoverPt =
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
if (CoverPt && CoverPt->bCrouch)
{
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true);
}
} }
} }

View File

@ -204,7 +204,6 @@ void APS_AI_Behavior_AIController::SetupBlackboard()
Blackboard->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex, 0); Blackboard->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex, 0);
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State, Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State,
static_cast<uint8>(EPS_AI_Behavior_State::Idle)); static_cast<uint8>(EPS_AI_Behavior_State::Idle));
Blackboard->SetValueAsBool(PS_AI_Behavior_BB::PreferCover, false);
} }
} }
@ -368,22 +367,14 @@ ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const A
} }
} }
// 3) Fallback: check if Pawn implements IPS_AI_Behavior_Interface → derive TeamId from NPCType + Faction // 3) Fallback: check if Pawn implements IPS_AI_Behavior_Interface → derive TeamId from NPCType
if (OtherTeam == FGenericTeamId::NoTeam && OtherPawn->Implements<UPS_AI_Behavior_Interface>()) if (OtherTeam == FGenericTeamId::NoTeam && OtherPawn->Implements<UPS_AI_Behavior_Interface>())
{ {
const EPS_AI_Behavior_NPCType OtherNPCType = const EPS_AI_Behavior_NPCType OtherNPCType =
IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(const_cast<APawn*>(OtherPawn)); IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(const_cast<APawn*>(OtherPawn));
if (OtherNPCType != EPS_AI_Behavior_NPCType::Any) if (OtherNPCType != EPS_AI_Behavior_NPCType::Any)
{ {
uint8 OtherFaction = 0; OtherTeam = PS_AI_Behavior_Team::MakeTeamId(OtherNPCType);
if (const auto* OtherPersonality = OtherPawn->FindComponentByClass<UPS_AI_Behavior_PersonalityComponent>())
{
if (OtherPersonality->Profile)
{
OtherFaction = OtherPersonality->Profile->Faction;
}
}
OtherTeam = PS_AI_Behavior_Team::MakeTeamId(OtherNPCType, OtherFaction);
} }
} }
} }

View File

@ -96,7 +96,7 @@ APawn* UPS_AI_Behavior_PerceptionComponent::FindOwningPawn(AActor* Actor)
return ActorAsPawn; return ActorAsPawn;
} }
// Not a Pawn — walk up Owner/Instigator/Attachment chain to find the owning Pawn // Not a Pawn — walk up Owner/Instigator chain to find the owning Pawn
AActor* Current = Actor; AActor* Current = Actor;
for (int32 Depth = 0; Depth < 4; ++Depth) // Safety limit for (int32 Depth = 0; Depth < 4; ++Depth) // Safety limit
{ {
@ -108,29 +108,14 @@ APawn* UPS_AI_Behavior_PerceptionComponent::FindOwningPawn(AActor* Actor)
// Try Owner // Try Owner
AActor* OwnerActor = Current->GetOwner(); AActor* OwnerActor = Current->GetOwner();
if (OwnerActor && OwnerActor != Current) if (!OwnerActor || OwnerActor == Current) break;
if (APawn* OwnerPawn = Cast<APawn>(OwnerActor))
{ {
if (APawn* OwnerPawn = Cast<APawn>(OwnerActor)) return OwnerPawn;
{
return OwnerPawn;
}
Current = OwnerActor;
continue;
} }
// Try attachment parent (ChildActorComponent → parent Character) Current = OwnerActor; // Continue up the chain
AActor* ParentActor = Current->GetAttachParentActor();
if (ParentActor && ParentActor != Current)
{
if (APawn* ParentPawn = Cast<APawn>(ParentActor))
{
return ParentPawn;
}
Current = ParentActor;
continue;
}
break; // No more chain to walk
} }
// Fallback: could not resolve to a Pawn. // Fallback: could not resolve to a Pawn.

View File

@ -44,9 +44,10 @@ void UPS_AI_Behavior_PersonalityComponent::BeginPlay()
else else
{ {
// Defaults — all traits at 0.5 // Defaults — all traits at 0.5
RuntimeTraits.Add(EPS_AI_Behavior_TraitAxis::Courage, 0.5f); for (uint8 i = 0; i <= static_cast<uint8>(EPS_AI_Behavior_TraitAxis::Discipline); ++i)
RuntimeTraits.Add(EPS_AI_Behavior_TraitAxis::Aggressivity, 0.5f); {
RuntimeTraits.Add(EPS_AI_Behavior_TraitAxis::Caution, 0.5f); RuntimeTraits.Add(static_cast<EPS_AI_Behavior_TraitAxis>(i), 0.5f);
}
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] No PersonalityProfile assigned — using default traits."), UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] No PersonalityProfile assigned — using default traits."),
*GetOwner()->GetName()); *GetOwner()->GetName());
} }
@ -93,32 +94,13 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
PerceivedThreatLevel, Aggressivity, Courage, Caution, PerceivedThreatLevel, Aggressivity, Courage, Caution,
EffectiveAttackThresh, EffectiveFleeThresh, AlertThresh); EffectiveAttackThresh, EffectiveFleeThresh, AlertThresh);
// Decision cascade — with hysteresis to prevent state flickering. // Decision cascade
// Once in a state, require a larger threshold drop to leave it. if (PerceivedThreatLevel >= EffectiveFleeThresh && Courage < 0.7f)
const float Hysteresis = 0.15f;
if (CurrentState == EPS_AI_Behavior_State::Fleeing)
{
// Stay fleeing until threat drops well below the threshold
if (PerceivedThreatLevel >= (EffectiveFleeThresh - Hysteresis))
{
return EPS_AI_Behavior_State::Fleeing;
}
}
else if (PerceivedThreatLevel >= EffectiveFleeThresh && Courage < 0.7f)
{ {
return EPS_AI_Behavior_State::Fleeing; return EPS_AI_Behavior_State::Fleeing;
} }
if (CurrentState == EPS_AI_Behavior_State::Combat) if (PerceivedThreatLevel >= EffectiveAttackThresh && Aggressivity > 0.3f)
{
// Stay in combat until threat drops well below the threshold
if (PerceivedThreatLevel >= (EffectiveAttackThresh - Hysteresis))
{
return EPS_AI_Behavior_State::Combat;
}
}
else if (PerceivedThreatLevel >= EffectiveAttackThresh && Aggressivity > 0.0f)
{ {
return EPS_AI_Behavior_State::Combat; return EPS_AI_Behavior_State::Combat;
} }

View File

@ -7,7 +7,9 @@ UPS_AI_Behavior_PersonalityProfile::UPS_AI_Behavior_PersonalityProfile()
// Initialize all trait axes to a neutral 0.5 // Initialize all trait axes to a neutral 0.5
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Courage, 0.5f); TraitScores.Add(EPS_AI_Behavior_TraitAxis::Courage, 0.5f);
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Aggressivity, 0.5f); TraitScores.Add(EPS_AI_Behavior_TraitAxis::Aggressivity, 0.5f);
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Loyalty, 0.5f);
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Caution, 0.5f); TraitScores.Add(EPS_AI_Behavior_TraitAxis::Caution, 0.5f);
TraitScores.Add(EPS_AI_Behavior_TraitAxis::Discipline, 0.5f);
// Default target priority: Protector first, then Player, then Civilian // Default target priority: Protector first, then Player, then Civilian
TargetPriority.Add(EPS_AI_Behavior_TargetType::Protector); TargetPriority.Add(EPS_AI_Behavior_TargetType::Protector);

View File

@ -13,11 +13,9 @@ class UEnvQuery;
/** /**
* BT Task: Move toward the threat actor and delegate combat to the Pawn. * BT Task: Move toward the threat actor and delegate combat to the Pawn.
* *
* Queries IPS_AI_Behavior_Interface for CombatType (Melee/Ranged). * Queries IPS_AI_Behavior_Interface for CombatType and OptimalAttackRange:
* Attack ranges come from PersonalityProfile (MinAttackRange/MaxAttackRange), * - Melee: rush toward target, stop at optimal range.
* with fallback to AttackMoveRadius if no profile is available. * - Ranged: maintain optimal distance back away if too close, advance if too far.
* - Melee: rush toward target, continuous pursuit within MinRange.
* - Ranged: maintain distance back away if closer than MinRange, advance if farther than MaxRange.
* *
* Calls IPS_AI_Behavior_Interface::BehaviorStartAttack() on enter and * Calls IPS_AI_Behavior_Interface::BehaviorStartAttack() on enter and
* BehaviorStopAttack() on abort. The Pawn handles the actual combat * BehaviorStopAttack() on abort. The Pawn handles the actual combat
@ -34,7 +32,7 @@ class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Attack : public UBTTaskNode
public: public:
UPS_AI_Behavior_BTTask_Attack(); UPS_AI_Behavior_BTTask_Attack();
/** Fallback attack range (cm) used when PersonalityProfile is unavailable. */ /** Fallback move radius if the Pawn doesn't implement GetBehaviorOptimalAttackRange(). */
UPROPERTY(EditAnywhere, Category = "Attack", meta = (ClampMin = "50.0")) UPROPERTY(EditAnywhere, Category = "Attack", meta = (ClampMin = "50.0"))
float AttackMoveRadius = 300.0f; float AttackMoveRadius = 300.0f;

View File

@ -43,19 +43,19 @@ public:
// ─── Timing ───────────────────────────────────────────────────────── // ─── Timing ─────────────────────────────────────────────────────────
/** Base minimum time (seconds) spent peeking/shooting. Modulated at runtime: Aggressivity increases, Caution decreases. */ /** Minimum time (seconds) spent peeking/shooting. */
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5")) UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
float PeekDurationMin = 2.0f; float PeekDurationMin = 2.0f;
/** Base maximum time (seconds) spent peeking/shooting. Modulated at runtime: Aggressivity increases, Caution decreases. */ /** Maximum time (seconds) spent peeking/shooting. */
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5")) UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
float PeekDurationMax = 5.0f; float PeekDurationMax = 5.0f;
/** Base minimum time (seconds) spent ducked behind cover. Modulated at runtime: Caution increases, Aggressivity decreases. */ /** Minimum time (seconds) spent ducked behind cover. */
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5")) UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
float CoverDurationMin = 1.0f; float CoverDurationMin = 1.0f;
/** Base maximum time (seconds) spent ducked behind cover. Modulated at runtime: Caution increases, Aggressivity decreases. */ /** Maximum time (seconds) spent ducked behind cover. */
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5")) UPROPERTY(EditAnywhere, Category = "Cover Shoot|Timing", meta = (ClampMin = "0.5"))
float CoverDurationMax = 3.0f; float CoverDurationMax = 3.0f;
@ -72,7 +72,7 @@ public:
// ─── Advancement ──────────────────────────────────────────────────── // ─── Advancement ────────────────────────────────────────────────────
/** Number of peek/duck cycles before attempting to advance to a closer cover point. Modulated by Aggressivity. */ /** Number of peek/duck cycles before advancing to a closer cover. */
UPROPERTY(EditAnywhere, Category = "Cover Shoot|Advancement", meta = (ClampMin = "1", ClampMax = "10")) UPROPERTY(EditAnywhere, Category = "Cover Shoot|Advancement", meta = (ClampMin = "1", ClampMax = "10"))
int32 MaxCyclesBeforeAdvance = 3; int32 MaxCyclesBeforeAdvance = 3;

View File

@ -51,9 +51,8 @@ public:
EPS_AI_Behavior_CoverPointType CoverPointType = EPS_AI_Behavior_CoverPointType::Cover; EPS_AI_Behavior_CoverPointType CoverPointType = EPS_AI_Behavior_CoverPointType::Cover;
/** /**
* Score bonus added to manual CoverPoints (additive, 0-1 range). * Bonus score added to manual CoverPoints over procedural candidates.
* A manual point with score 0.5 + bonus 0.3 = 0.8 total. * Higher = manual points are strongly preferred.
* Higher = manual points strongly preferred over procedural candidates.
*/ */
UPROPERTY(EditAnywhere, Category = "Cover|Manual Points", meta = (ClampMin = "0.0", ClampMax = "1.0")) UPROPERTY(EditAnywhere, Category = "Cover|Manual Points", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float ManualPointBonus = 0.3f; float ManualPointBonus = 0.3f;

View File

@ -33,15 +33,15 @@ public:
/** /**
* Team ID determines perception affiliation (Enemy/Friendly/Neutral). * Team ID determines perception affiliation (Enemy/Friendly/Neutral).
* Auto-assigned from NPCType + Faction at possession via MakeTeamId(): * Auto-assigned from NPCType at possession if left at 255 (NoTeam):
* - Encoding: high nibble = NPCType, low nibble = Faction * - Civilian = Team 1
* - Civilian F0 = 0x00, Enemy F0 = 0x10, Protector F0 = 0x20 * - Enemy = Team 2
* - Same NPCType + same Faction Friendly * - Neutral = 255 (NoTeam perceived as Neutral by everyone)
* - Same NPCType + different Faction Hostile (rival gangs) *
* - Civilian Protector always Friendly * Two NPCs with the SAME Team ID Friendly (ignored by perception).
* - Everything else Hostile * Two NPCs with DIFFERENT Team IDs Enemy (detected by perception).
* A NPC with Team ID 255 Neutral to everyone.
* *
* Disguised enemies (hostile=false) use DisguisedTeamId (0x01).
* You can override this in Blueprint or per-instance in the editor. * You can override this in Blueprint or per-instance in the editor.
*/ */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Team") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Team")

View File

@ -27,11 +27,11 @@ public:
// ─── Configuration ────────────────────────────────────────────────── // ─── Configuration ──────────────────────────────────────────────────
/** Maximum distance at which ExecuteAttack() succeeds (cm). Independent from PersonalityProfile ranges used by BTTask_Attack. */ /** Maximum distance at which the NPC can attack (cm). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat", meta = (ClampMin = "50.0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat", meta = (ClampMin = "50.0"))
float AttackRange = 200.0f; float AttackRange = 200.0f;
/** Minimum time between consecutive ExecuteAttack() calls (seconds). */ /** Cooldown between attacks (seconds). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat", meta = (ClampMin = "0.1")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat", meta = (ClampMin = "0.1"))
float AttackCooldown = 1.5f; float AttackCooldown = 1.5f;

View File

@ -94,9 +94,11 @@ enum class EPS_AI_Behavior_CombatSubState : uint8
UENUM(BlueprintType) UENUM(BlueprintType)
enum class EPS_AI_Behavior_TraitAxis : uint8 enum class EPS_AI_Behavior_TraitAxis : uint8
{ {
Courage UMETA(DisplayName = "Courage", ToolTip = "0 = coward, 1 = fearless. Gates flee threshold and cover advancement."), Courage UMETA(DisplayName = "Courage", ToolTip = "0 = coward, 1 = fearless"),
Aggressivity UMETA(DisplayName = "Aggressivity", ToolTip = "0 = peaceful, 1 = violent. Drives attack threshold and Combat/Cover time ratio."), Aggressivity UMETA(DisplayName = "Aggressivity", ToolTip = "0 = peaceful, 1 = violent"),
Caution UMETA(DisplayName = "Caution", ToolTip = "0 = reckless, 1 = prudent. Drives flee threshold, cover duration, and spline threat avoidance."), Loyalty UMETA(DisplayName = "Loyalty", ToolTip = "0 = selfish, 1 = devoted"),
Caution UMETA(DisplayName = "Caution", ToolTip = "0 = reckless, 1 = prudent"),
Discipline UMETA(DisplayName = "Discipline", ToolTip = "0 = undisciplined, 1 = disciplined"),
}; };
// ─── TeamId Encoding ──────────────────────────────────────────────────────── // ─── TeamId Encoding ────────────────────────────────────────────────────────

View File

@ -79,15 +79,6 @@ public:
meta = (ClampMin = "0.0", ClampMax = "1.0")) meta = (ClampMin = "0.0", ClampMax = "1.0"))
float AlertThreshold = 0.15f; float AlertThreshold = 0.15f;
/**
* How fast the perceived threat level decays per second when no active threat is perceived.
* Low values = NPC stays scared/alert longer (civilians).
* High values = NPC calms down quickly (trained soldiers).
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Personality|Reaction",
meta = (ClampMin = "0.001", ClampMax = "1.0"))
float ThreatDecayRate = 0.05f;
// ─── Target Priority (Combat) ─────────────────────────────────────── // ─── Target Priority (Combat) ───────────────────────────────────────
/** /**