Compare commits
4 Commits
69b9844a4b
...
d471714fbd
| Author | SHA1 | Date | |
|---|---|---|---|
| d471714fbd | |||
| af93194f48 | |||
| f5897b46cc | |||
| de3a5310f4 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
@ -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) ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user