Compare commits

...

4 Commits

Author SHA1 Message Date
d471714fbd Fix rival faction detection: include Faction in fallback TeamId resolution
- GetTeamAttitudeTowards fallback (interface path) was calling MakeTeamId
  without Faction, so enemies of different factions had identical TeamIds
  → always Friendly instead of Hostile
- Now reads Faction from PersonalityProfile when resolving via interface

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:14:36 +02:00
af93194f48 Multiple fixes: threat decay per profile, state hysteresis, spline pathfinding, attack readiness
- Move ThreatDecayRate from global Settings to PersonalityProfile (per-archetype)
- Add state hysteresis in EvaluateReaction to prevent Fleeing/Combat flickering
- FindCover: verify distance on arrival, retry movement if too far
- FindAndFollowSpline: sample multiple spline points when closest is blocked
- BTTask_Attack: call BehaviorStartAttack immediately (draw weapon before LOS/range)
- CoverShootCycle: call BehaviorStartAttack on entry (weapon ready during cover approach)
- FindOwningPawn: walk AttachParentActor chain for ChildActor weapons without Owner
- Initialize PreferCover BB key to false at setup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:42:05 +02:00
f5897b46cc Remove unused Loyalty/Discipline traits, lower Aggressivity combat threshold to 0
- Remove Loyalty and Discipline from EPS_AI_Behavior_TraitAxis enum (never used in gameplay)
- Clean up PersonalityProfile and PersonalityComponent default trait initialization
- Add descriptive tooltips to remaining traits (Courage, Aggressivity, Caution)
- Lower Aggressivity combat gate from 0.3 to 0.0 (only Aggressivity=0 prevents combat)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 07:16:19 +02:00
de3a5310f4 Update tooltips and comments: fix outdated references, clarify personality modulation
- BTTask_Attack.h: remove reference to non-existent GetBehaviorOptimalAttackRange, document PersonalityProfile ranges
- AIController.h: update TeamId comment to reflect actual MakeTeamId encoding (nibble-based)
- CoverShootCycle.h: clarify that Peek/Cover durations are base values modulated by personality traits
- FindCover.h: clarify ManualPointBonus is additive score
- CombatComponent.h: clarify AttackRange/AttackCooldown are for ExecuteAttack, not BTTask_Attack

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 07:04:59 +02:00
31 changed files with 208 additions and 82 deletions

View File

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

View File

@ -103,17 +103,19 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
const float ApproachRange = (Memory->CombatType == EPS_AI_Behavior_CombatType::Melee)
? 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
const float DistToTarget = FVector::Dist(Pawn->GetActorLocation(), Target->GetActorLocation());
if (DistToTarget <= Memory->MaxRange)
{
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

View File

@ -132,6 +132,13 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_CoverShootCycle::ExecuteTask(
// Face the threat while in cover
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)
{
Memory->SubState = EPS_AI_Behavior_CombatSubState::AtCover;

View File

@ -148,34 +148,64 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_FindAndFollowSpline::ExecuteTask(
if (bWalkToSpline && GapToSpline > AcceptanceRadius)
{
// Walk to spline first via NavMesh
const EPathFollowingRequestResult::Type Result = AIC->MoveToLocation(
SplinePoint, AcceptanceRadius, /*bStopOnOverlap=*/true,
/*bUsePathfinding=*/true, /*bProjectDestinationToNavigation=*/true,
/*bCanStrafe=*/false);
// Try to reach the closest spline point. If blocked, sample other points along the spline.
const float SplineLength = ClosestSpline->GetSplineLength();
const int32 NumSamples = 8;
const float SampleStep = SplineLength / NumSamples;
if (Result == EPathFollowingRequestResult::Failed)
// Start with the closest point, then try evenly spaced samples
TArray<float> DistancesToTry;
DistancesToTry.Add(DistAlongSpline);
for (int32 i = 1; i <= NumSamples; ++i)
{
// Can't reach via NavMesh — try starting anyway (snap)
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;
const float D = FMath::Fmod(DistAlongSpline + i * SampleStep, SplineLength);
DistancesToTry.Add(D);
}
if (Result == EPathFollowingRequestResult::AlreadyAtGoal)
float ReachableDist = -1.0f;
FVector ReachablePoint = FVector::ZeroVector;
for (float TestDist : DistancesToTry)
{
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;
const FVector TestPoint = ClosestSpline->GetWorldLocationAtDistance(TestDist);
const EPathFollowingRequestResult::Type TestResult = AIC->MoveToLocation(
TestPoint, AcceptanceRadius, true, true, true, false);
if (TestResult == EPathFollowingRequestResult::AlreadyAtGoal)
{
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();
}
// Store the spline to connect to after reaching it
if (ReachableDist < 0.0f)
{
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->CurrentDistance = DistAlongSpline;
Follower->CurrentDistance = ReachableDist;
FFindSplineMemory* Memory = reinterpret_cast<FFindSplineMemory*>(NodeMemory);
Memory->bMovingToSpline = true;

View File

@ -207,21 +207,51 @@ void UPS_AI_Behavior_BTTask_FindCover::TickTask(
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;
// Crouch at cover if the point requires it
APawn* Pawn = AIC->GetPawn();
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>() && BB)
{
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
if (BB)
const APS_AI_Behavior_CoverPoint* CoverPt =
Cast<APS_AI_Behavior_CoverPoint>(BB->GetValueAsObject(PS_AI_Behavior_BB::CoverPoint));
if (CoverPt && CoverPt->bCrouch)
{
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);
}
IPS_AI_Behavior_Interface::Execute_SetBehaviorCrouch(Pawn, true);
}
}

View File

@ -204,6 +204,7 @@ void APS_AI_Behavior_AIController::SetupBlackboard()
Blackboard->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex, 0);
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State,
static_cast<uint8>(EPS_AI_Behavior_State::Idle));
Blackboard->SetValueAsBool(PS_AI_Behavior_BB::PreferCover, false);
}
}
@ -367,14 +368,22 @@ ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const A
}
}
// 3) Fallback: check if Pawn implements IPS_AI_Behavior_Interface → derive TeamId from NPCType
// 3) Fallback: check if Pawn implements IPS_AI_Behavior_Interface → derive TeamId from NPCType + Faction
if (OtherTeam == FGenericTeamId::NoTeam && OtherPawn->Implements<UPS_AI_Behavior_Interface>())
{
const EPS_AI_Behavior_NPCType OtherNPCType =
IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(const_cast<APawn*>(OtherPawn));
if (OtherNPCType != EPS_AI_Behavior_NPCType::Any)
{
OtherTeam = PS_AI_Behavior_Team::MakeTeamId(OtherNPCType);
uint8 OtherFaction = 0;
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;
}
// Not a Pawn — walk up Owner/Instigator chain to find the owning Pawn
// Not a Pawn — walk up Owner/Instigator/Attachment chain to find the owning Pawn
AActor* Current = Actor;
for (int32 Depth = 0; Depth < 4; ++Depth) // Safety limit
{
@ -108,14 +108,29 @@ APawn* UPS_AI_Behavior_PerceptionComponent::FindOwningPawn(AActor* Actor)
// Try Owner
AActor* OwnerActor = Current->GetOwner();
if (!OwnerActor || OwnerActor == Current) break;
if (APawn* OwnerPawn = Cast<APawn>(OwnerActor))
if (OwnerActor && OwnerActor != Current)
{
return OwnerPawn;
if (APawn* OwnerPawn = Cast<APawn>(OwnerActor))
{
return OwnerPawn;
}
Current = OwnerActor;
continue;
}
Current = OwnerActor; // Continue up the chain
// Try attachment parent (ChildActorComponent → parent Character)
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -79,6 +79,15 @@ public:
meta = (ClampMin = "0.0", ClampMax = "1.0"))
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) ───────────────────────────────────────
/**