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)"),
|
||||
*AIC->GetName(), AIC->GetGenericTeamId().GetId(), ExpectedTeamId, (int32)bHostile);
|
||||
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());
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (Blackboard)
|
||||
|
||||
@@ -77,6 +77,21 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
||||
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 Aggressivity = GetTrait(EPS_AI_Behavior_TraitAxis::Aggressivity);
|
||||
const float Caution = GetTrait(EPS_AI_Behavior_TraitAxis::Caution);
|
||||
@@ -119,6 +134,9 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
||||
return EPS_AI_Behavior_State::Fleeing;
|
||||
}
|
||||
|
||||
// Disguised enemies never enter active combat — skip the Combat branch entirely.
|
||||
if (!bDisguisedEnemy)
|
||||
{
|
||||
if (CurrentState == EPS_AI_Behavior_State::Combat)
|
||||
{
|
||||
// Stay in combat until threat drops well below the threshold
|
||||
@@ -131,6 +149,7 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
||||
{
|
||||
return EPS_AI_Behavior_State::Combat;
|
||||
}
|
||||
}
|
||||
|
||||
if (PerceivedThreatLevel >= AlertThresh)
|
||||
{
|
||||
@@ -280,6 +299,22 @@ float UPS_AI_Behavior_PersonalityComponent::CalculatePhaseDuration(EPS_AI_Behavi
|
||||
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)
|
||||
{
|
||||
// Only server can change replicated state
|
||||
|
||||
@@ -115,6 +115,25 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior")
|
||||
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:
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
virtual void OnUnPossess() override;
|
||||
|
||||
@@ -113,6 +113,14 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior|Personality")
|
||||
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. */
|
||||
UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_AI_Behavior|Personality")
|
||||
EPS_AI_Behavior_NPCType GetNPCType() const;
|
||||
|
||||
Reference in New Issue
Block a user