diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_EvaluateReaction.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_EvaluateReaction.cpp index f7aadfb..d0574db 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_EvaluateReaction.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTService_EvaluateReaction.cpp @@ -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()); + } } } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp index aa309c1..d1d7c08 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_AIController.cpp @@ -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()) + { + 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()) + { + 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() && + !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(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) diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PersonalityComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PersonalityComponent.cpp index f171d06..48f8519 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PersonalityComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/PS_AI_Behavior_PersonalityComponent.cpp @@ -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(GetOwner())) + { + if (OwnerPawn->Implements()) + { + 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,18 +134,22 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c 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 (PerceivedThreatLevel >= (EffectiveAttackThresh - Hysteresis)) + 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; } } - else if (PerceivedThreatLevel >= EffectiveAttackThresh && Aggressivity > 0.0f) - { - 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 diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_AIController.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_AIController.h index 450bb80..174f775 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_AIController.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_AIController.h @@ -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; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_PersonalityComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_PersonalityComponent.h index a8501f4..ffef453 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_PersonalityComponent.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_PersonalityComponent.h @@ -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;