Add ResetBehavior() and de-escalation on hostile flip
- AIController::ResetBehavior() — immediate reset to initial-tick state: stops movement, clears all BB threat/cover/patrol keys, recomputes TeamId, forces state to Idle (triggers speed update via HandleStateChanged). For civilian calm-down, enemy surrender, mission reset. - PersonalityComponent::ResetRuntimeState() — clears PerceivedThreatLevel and combat/cover cycle internals. Traits preserved. - PersonalityComponent::EvaluateReaction() — disguised Enemy (hostile=false) never returns Combat regardless of threat level. Prevents stuck-in-combat bug when hostility flips mid-fight. - BTService_EvaluateReaction — when hostile flips true→false, soft de-escalation: clears threat so next ApplyReaction transitions to Idle naturally (no abrupt movement stop). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,24 @@ void UPS_AI_Behavior_BTService_EvaluateReaction::TickNode(
|
|||||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Hostility changed: TeamId 0x%02X -> 0x%02X (hostile=%d)"),
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Hostility changed: TeamId 0x%02X -> 0x%02X (hostile=%d)"),
|
||||||
*AIC->GetName(), AIC->GetGenericTeamId().GetId(), ExpectedTeamId, (int32)bHostile);
|
*AIC->GetName(), AIC->GetGenericTeamId().GetId(), ExpectedTeamId, (int32)bHostile);
|
||||||
AIC->SetTeamId(ExpectedTeamId);
|
AIC->SetTeamId(ExpectedTeamId);
|
||||||
|
|
||||||
|
// Enemy becoming non-hostile (disguise) — soft de-escalation:
|
||||||
|
// clear threat so the BT re-evaluates to Idle naturally on the next
|
||||||
|
// ApplyReaction below. Do NOT stop movement or force speed abruptly —
|
||||||
|
// the natural state transition will handle that via HandleStateChanged.
|
||||||
|
if (NPCType == EPS_AI_Behavior_NPCType::Enemy && !bHostile)
|
||||||
|
{
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||||
|
BB->ClearValue(PS_AI_Behavior_BB::ThreatLocation);
|
||||||
|
BB->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
||||||
|
if (Personality)
|
||||||
|
{
|
||||||
|
Personality->PerceivedThreatLevel = 0.0f;
|
||||||
|
}
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log,
|
||||||
|
TEXT("[%s] Hostile→false: threat cleared, BT will transition naturally."),
|
||||||
|
*AIC->GetName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -324,6 +324,111 @@ void APS_AI_Behavior_AIController::StopBehavior()
|
|||||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] StopBehavior — BT stopped, entering Scripted state."), *GetName());
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] StopBehavior — BT stopped, entering Scripted state."), *GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APS_AI_Behavior_AIController::ResetBehavior()
|
||||||
|
{
|
||||||
|
APawn* MyPawn = GetPawn();
|
||||||
|
if (!MyPawn)
|
||||||
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] ResetBehavior: no Pawn."), *GetName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 1. Stop any active movement ────────────────────────────────────
|
||||||
|
StopMovement();
|
||||||
|
|
||||||
|
if (auto* Spline = MyPawn->FindComponentByClass<UPS_AI_Behavior_SplineFollowerComponent>())
|
||||||
|
{
|
||||||
|
Spline->PauseFollowing();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 2. Reset PersonalityComponent runtime state ────────────────────
|
||||||
|
if (PersonalityComp)
|
||||||
|
{
|
||||||
|
PersonalityComp->ResetRuntimeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 3. Recompute TeamId from current NPCType + Faction + Hostile ──
|
||||||
|
{
|
||||||
|
EPS_AI_Behavior_NPCType NPCType = EPS_AI_Behavior_NPCType::Any;
|
||||||
|
if (MyPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
|
{
|
||||||
|
NPCType = IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(MyPawn);
|
||||||
|
}
|
||||||
|
else if (PersonalityComp)
|
||||||
|
{
|
||||||
|
NPCType = PersonalityComp->GetNPCType();
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8 Faction = (PersonalityComp && PersonalityComp->Profile)
|
||||||
|
? PersonalityComp->Profile->Faction : 0;
|
||||||
|
|
||||||
|
uint8 NewTeamId;
|
||||||
|
if (NPCType == EPS_AI_Behavior_NPCType::Enemy &&
|
||||||
|
MyPawn->Implements<UPS_AI_Behavior_Interface>() &&
|
||||||
|
!IPS_AI_Behavior_Interface::Execute_IsBehaviorHostile(MyPawn))
|
||||||
|
{
|
||||||
|
NewTeamId = PS_AI_Behavior_Team::DisguisedTeamId;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NewTeamId = PS_AI_Behavior_Team::MakeTeamId(NPCType, Faction);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetTeamId(NewTeamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 4. Reset Blackboard keys to initial-tick defaults ──────────────
|
||||||
|
if (Blackboard)
|
||||||
|
{
|
||||||
|
// Threat
|
||||||
|
Blackboard->ClearValue(PS_AI_Behavior_BB::ThreatActor);
|
||||||
|
Blackboard->ClearValue(PS_AI_Behavior_BB::ThreatLocation);
|
||||||
|
Blackboard->SetValueAsFloat(PS_AI_Behavior_BB::ThreatLevel, 0.0f);
|
||||||
|
Blackboard->ClearValue(PS_AI_Behavior_BB::LastKnownTargetPosition);
|
||||||
|
Blackboard->ClearValue(PS_AI_Behavior_BB::ThreatPawnName);
|
||||||
|
|
||||||
|
// Cover
|
||||||
|
Blackboard->ClearValue(PS_AI_Behavior_BB::CoverLocation);
|
||||||
|
Blackboard->ClearValue(PS_AI_Behavior_BB::CoverPoint);
|
||||||
|
Blackboard->SetValueAsBool(PS_AI_Behavior_BB::PreferCover, false);
|
||||||
|
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::CombatSubState,
|
||||||
|
static_cast<uint8>(EPS_AI_Behavior_CombatSubState::Engaging));
|
||||||
|
|
||||||
|
// Patrol / Spline
|
||||||
|
Blackboard->SetValueAsInt(PS_AI_Behavior_BB::PatrolIndex, 0);
|
||||||
|
Blackboard->ClearValue(PS_AI_Behavior_BB::CurrentSpline);
|
||||||
|
Blackboard->SetValueAsFloat(PS_AI_Behavior_BB::SplineProgress, 0.0f);
|
||||||
|
|
||||||
|
// Re-capture HomeLocation to current actor location (initial-tick semantics)
|
||||||
|
Blackboard->SetValueAsVector(PS_AI_Behavior_BB::HomeLocation,
|
||||||
|
MyPawn->GetActorLocation());
|
||||||
|
|
||||||
|
// Conversation pause
|
||||||
|
Blackboard->SetValueAsBool(PS_AI_Behavior_BB::ConversationPaused, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 5. Clear local gaze / conversation caches ──────────────────────
|
||||||
|
bConversationPaused = false;
|
||||||
|
bUserHasSpokenInConversation = false;
|
||||||
|
ProximityGazeTarget = nullptr;
|
||||||
|
ProximityGazeDuration = 0.0f;
|
||||||
|
ProximityGazeCooldownTimer = 0.0f;
|
||||||
|
ProximityGazeCooldownActor = nullptr;
|
||||||
|
ClearGazeTarget();
|
||||||
|
|
||||||
|
// ─── 6. Force state back to Idle ────────────────────────────────────
|
||||||
|
// ForceState on PersonalityComp updates CurrentState + triggers HandleStateChanged
|
||||||
|
// which calls SetBehaviorMovementSpeed on the Pawn (so walk speed is reset).
|
||||||
|
// SetBehaviorState syncs the Blackboard for the BT.
|
||||||
|
if (PersonalityComp)
|
||||||
|
{
|
||||||
|
PersonalityComp->ForceState(EPS_AI_Behavior_State::Idle);
|
||||||
|
}
|
||||||
|
SetBehaviorState(EPS_AI_Behavior_State::Idle);
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] ResetBehavior — back to initial-tick state (Idle)."), *GetName());
|
||||||
|
}
|
||||||
|
|
||||||
void APS_AI_Behavior_AIController::SetBehaviorState(EPS_AI_Behavior_State NewState)
|
void APS_AI_Behavior_AIController::SetBehaviorState(EPS_AI_Behavior_State NewState)
|
||||||
{
|
{
|
||||||
if (Blackboard)
|
if (Blackboard)
|
||||||
|
|||||||
@@ -77,6 +77,21 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
|||||||
return EPS_AI_Behavior_State::Scripted;
|
return EPS_AI_Behavior_State::Scripted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disguised Enemy (NPCType=Enemy but IsBehaviorHostile=false) — cannot enter
|
||||||
|
// active combat states. Behave like a civilian (Alerted max, or Fleeing if terrified).
|
||||||
|
bool bDisguisedEnemy = false;
|
||||||
|
if (APawn* OwnerPawn = Cast<APawn>(GetOwner()))
|
||||||
|
{
|
||||||
|
if (OwnerPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||||
|
{
|
||||||
|
const EPS_AI_Behavior_NPCType NPCT =
|
||||||
|
IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(OwnerPawn);
|
||||||
|
const bool bHostile =
|
||||||
|
IPS_AI_Behavior_Interface::Execute_IsBehaviorHostile(OwnerPawn);
|
||||||
|
bDisguisedEnemy = (NPCT == EPS_AI_Behavior_NPCType::Enemy && !bHostile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const float Courage = GetTrait(EPS_AI_Behavior_TraitAxis::Courage);
|
const float Courage = GetTrait(EPS_AI_Behavior_TraitAxis::Courage);
|
||||||
const float Aggressivity = GetTrait(EPS_AI_Behavior_TraitAxis::Aggressivity);
|
const float Aggressivity = GetTrait(EPS_AI_Behavior_TraitAxis::Aggressivity);
|
||||||
const float Caution = GetTrait(EPS_AI_Behavior_TraitAxis::Caution);
|
const float Caution = GetTrait(EPS_AI_Behavior_TraitAxis::Caution);
|
||||||
@@ -119,18 +134,22 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
|||||||
return EPS_AI_Behavior_State::Fleeing;
|
return EPS_AI_Behavior_State::Fleeing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CurrentState == EPS_AI_Behavior_State::Combat)
|
// Disguised enemies never enter active combat — skip the Combat branch entirely.
|
||||||
|
if (!bDisguisedEnemy)
|
||||||
{
|
{
|
||||||
// Stay in combat until threat drops well below the threshold
|
if (CurrentState == EPS_AI_Behavior_State::Combat)
|
||||||
if (PerceivedThreatLevel >= (EffectiveAttackThresh - Hysteresis))
|
{
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (PerceivedThreatLevel >= EffectiveAttackThresh && Aggressivity > 0.0f)
|
|
||||||
{
|
|
||||||
return EPS_AI_Behavior_State::Combat;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PerceivedThreatLevel >= AlertThresh)
|
if (PerceivedThreatLevel >= AlertThresh)
|
||||||
{
|
{
|
||||||
@@ -280,6 +299,22 @@ float UPS_AI_Behavior_PersonalityComponent::CalculatePhaseDuration(EPS_AI_Behavi
|
|||||||
return Duration;
|
return Duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_AI_Behavior_PersonalityComponent::ResetRuntimeState()
|
||||||
|
{
|
||||||
|
if (!GetOwner() || !GetOwner()->HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PerceivedThreatLevel = 0.0f;
|
||||||
|
CombatCoverTimer = 0.0f;
|
||||||
|
bCombatCoverCycleActive = false;
|
||||||
|
bPreferCover = false;
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] PersonalityComponent runtime state reset."),
|
||||||
|
*GetOwner()->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_AI_Behavior_PersonalityComponent::ForceState(EPS_AI_Behavior_State NewState)
|
void UPS_AI_Behavior_PersonalityComponent::ForceState(EPS_AI_Behavior_State NewState)
|
||||||
{
|
{
|
||||||
// Only server can change replicated state
|
// Only server can change replicated state
|
||||||
|
|||||||
@@ -115,6 +115,25 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior")
|
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior")
|
||||||
void StopBehavior();
|
void StopBehavior();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the NPC to its initial post-possession state (as if OnPossess had just run).
|
||||||
|
* Works for any NPC type (civilian, enemy, protector).
|
||||||
|
*
|
||||||
|
* Resets:
|
||||||
|
* - Blackboard: all threat, cover, patrol, spline keys back to defaults
|
||||||
|
* - Blackboard: HomeLocation re-captured to current actor location
|
||||||
|
* - PersonalityComponent: PerceivedThreatLevel = 0, combat/cover cycle cleared
|
||||||
|
* - TeamId: recomputed from current NPCType + Faction + Hostile flag
|
||||||
|
* - BehaviorState: forced to Idle (BT transitions to Patrol if on a spline)
|
||||||
|
* - Movement: any pending MoveTo cancelled, spline follower paused/reset
|
||||||
|
*
|
||||||
|
* Does NOT reset: runtime traits (ModifyTrait changes are kept), BT (stays running).
|
||||||
|
*
|
||||||
|
* Use cases: civilian calmed down by dialogue, enemy surrendering, mission reset.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior")
|
||||||
|
void ResetBehavior();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void OnPossess(APawn* InPawn) override;
|
virtual void OnPossess(APawn* InPawn) override;
|
||||||
virtual void OnUnPossess() override;
|
virtual void OnUnPossess() override;
|
||||||
|
|||||||
@@ -113,6 +113,14 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior|Personality")
|
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior|Personality")
|
||||||
void ForceState(EPS_AI_Behavior_State NewState);
|
void ForceState(EPS_AI_Behavior_State NewState);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset runtime state — perceived threat level, combat/cover cycle.
|
||||||
|
* Does NOT reset traits (ModifyTrait changes are preserved).
|
||||||
|
* Called by AIController::ResetBehavior().
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior|Personality")
|
||||||
|
void ResetRuntimeState();
|
||||||
|
|
||||||
/** Get the NPC type from the interface or profile. */
|
/** Get the NPC type from the interface or profile. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior|Personality")
|
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior|Personality")
|
||||||
EPS_AI_Behavior_NPCType GetNPCType() const;
|
EPS_AI_Behavior_NPCType GetNPCType() const;
|
||||||
|
|||||||
Reference in New Issue
Block a user