Fix BT state transitions, attack persistence, and hostile team switching
BT Attack task: - Stay InProgress permanently instead of returning Succeeded after each attack - Stay InProgress on MoveToActor failure instead of Failed (retry next tick) - Add verbose logging for attack state (target, range, distance) BT EvaluateReaction service: - Auto-detect hostility changes via IPS_AI_Behavior interface - Dynamically update TeamId when IsBehaviorHostile() changes (infiltrator reveal) AIController: - Remove GetBehaviorTeamId from interface (TeamId is now 100% automatic) - TeamId derived from NPCType + hostile state, no user implementation needed - Add BB State change logging for debug - Use SetValueAsEnum consistently for BehaviorState key Interface: - Remove GetBehaviorTeamId — TeamId is computed by the plugin automatically Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2e04cb0334
commit
9d054cc46f
@ -2,6 +2,7 @@
|
||||
|
||||
#include "BT/PS_AI_Behavior_BTService_EvaluateReaction.h"
|
||||
#include "PS_AI_Behavior_AIController.h"
|
||||
#include "PS_AI_Behavior_Interface.h"
|
||||
#include "PS_AI_Behavior_PersonalityComponent.h"
|
||||
#include "PS_AI_Behavior_Definitions.h"
|
||||
#include "BehaviorTree/BlackboardComponent.h"
|
||||
@ -27,7 +28,33 @@ void UPS_AI_Behavior_BTService_EvaluateReaction::TickNode(
|
||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||
if (!BB) return;
|
||||
|
||||
// Evaluate and apply the reaction
|
||||
// ─── Check for hostility change → update TeamId dynamically ────────
|
||||
APawn* Pawn = AIC->GetPawn();
|
||||
if (Pawn && Pawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
const bool bHostile = IPS_AI_Behavior_Interface::Execute_IsBehaviorHostile(Pawn);
|
||||
const EPS_AI_Behavior_NPCType NPCType = IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(Pawn);
|
||||
|
||||
// An infiltrated Enemy (hostile=false) has TeamId=1 (civilian disguise).
|
||||
// When hostile flips to true, switch to TeamId=2 (enemy).
|
||||
uint8 ExpectedTeamId;
|
||||
switch (NPCType)
|
||||
{
|
||||
case EPS_AI_Behavior_NPCType::Civilian: ExpectedTeamId = 1; break;
|
||||
case EPS_AI_Behavior_NPCType::Enemy: ExpectedTeamId = bHostile ? 2 : 1; break;
|
||||
case EPS_AI_Behavior_NPCType::Protector: ExpectedTeamId = 3; break;
|
||||
default: ExpectedTeamId = FGenericTeamId::NoTeam; break;
|
||||
}
|
||||
|
||||
if (AIC->GetGenericTeamId().GetId() != ExpectedTeamId)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Hostility changed: TeamId %d -> %d (hostile=%d)"),
|
||||
*AIC->GetName(), AIC->GetGenericTeamId().GetId(), ExpectedTeamId, (int32)bHostile);
|
||||
AIC->SetTeamId(ExpectedTeamId);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Evaluate and apply the reaction ────────────────────────────────
|
||||
const EPS_AI_Behavior_State NewState = Personality->ApplyReaction();
|
||||
|
||||
// Write to Blackboard
|
||||
|
||||
@ -27,6 +27,7 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
||||
AActor* Target = Cast<AActor>(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor));
|
||||
if (!Target)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: no ThreatActor in BB."), *AIC->GetName());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
@ -36,19 +37,23 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
||||
if (!Combat)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||
TEXT("[%s] Attack task: no CombatComponent on Pawn."), *AIC->GetName());
|
||||
TEXT("[%s] Attack: no CombatComponent on Pawn."), *AIC->GetName());
|
||||
return EBTNodeResult::Failed;
|
||||
}
|
||||
|
||||
UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Attack: target='%s', range=%.0f, dist=%.0f, inRange=%d, canAttack=%d"),
|
||||
*AIC->GetName(), *Target->GetName(), Combat->AttackRange,
|
||||
FVector::Dist(AIC->GetPawn()->GetActorLocation(), Target->GetActorLocation()),
|
||||
(int32)Combat->IsInAttackRange(Target), (int32)Combat->CanAttack());
|
||||
|
||||
// Try to attack immediately if in range
|
||||
if (Combat->IsInAttackRange(Target))
|
||||
{
|
||||
if (Combat->CanAttack())
|
||||
{
|
||||
Combat->ExecuteAttack(Target);
|
||||
return EBTNodeResult::Succeeded;
|
||||
}
|
||||
// In range but on cooldown — wait
|
||||
// Stay InProgress — keep attacking while in combat state
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
@ -59,7 +64,9 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask(
|
||||
|
||||
if (Result == EPathFollowingRequestResult::Failed)
|
||||
{
|
||||
return EBTNodeResult::Failed;
|
||||
UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] Attack: MoveToActor failed — no path to target."), *AIC->GetName());
|
||||
// Stay InProgress anyway — will retry next tick instead of giving up
|
||||
return EBTNodeResult::InProgress;
|
||||
}
|
||||
|
||||
FAttackMemory* Memory = reinterpret_cast<FAttackMemory*>(NodeMemory);
|
||||
@ -107,9 +114,9 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask(
|
||||
if (Combat->CanAttack())
|
||||
{
|
||||
Combat->ExecuteAttack(Target);
|
||||
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
|
||||
}
|
||||
// Else: wait for cooldown (stay InProgress)
|
||||
// Stay InProgress — keep attacking (cooldown handles the rate)
|
||||
// Observer Aborts on the Decorator will pull us out when state changes
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@ -48,13 +48,6 @@ void APS_AI_Behavior_AIController::OnPossess(APawn* InPawn)
|
||||
{
|
||||
// Use the interface — the host project controls the storage
|
||||
NPCType = IPS_AI_Behavior_Interface::Execute_GetBehaviorNPCType(InPawn);
|
||||
|
||||
// Also check if the interface provides a specific TeamId
|
||||
const uint8 InterfaceTeamId = IPS_AI_Behavior_Interface::Execute_GetBehaviorTeamId(InPawn);
|
||||
if (InterfaceTeamId != FGenericTeamId::NoTeam)
|
||||
{
|
||||
TeamId = InterfaceTeamId;
|
||||
}
|
||||
}
|
||||
else if (PersonalityComp)
|
||||
{
|
||||
@ -227,7 +220,16 @@ void APS_AI_Behavior_AIController::SetBehaviorState(EPS_AI_Behavior_State NewSta
|
||||
{
|
||||
if (Blackboard)
|
||||
{
|
||||
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State, static_cast<uint8>(NewState));
|
||||
const uint8 OldVal = Blackboard->GetValueAsEnum(PS_AI_Behavior_BB::State);
|
||||
const uint8 NewVal = static_cast<uint8>(NewState);
|
||||
if (OldVal != NewVal)
|
||||
{
|
||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] BB State: %s -> %s"),
|
||||
*GetName(),
|
||||
*UEnum::GetValueAsString(static_cast<EPS_AI_Behavior_State>(OldVal)),
|
||||
*UEnum::GetValueAsString(NewState));
|
||||
}
|
||||
Blackboard->SetValueAsEnum(PS_AI_Behavior_BB::State, NewVal);
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,16 +276,12 @@ ETeamAttitude::Type APS_AI_Behavior_AIController::GetTeamAttitudeTowards(const A
|
||||
|
||||
if (OtherPawn)
|
||||
{
|
||||
// Check via AIController first
|
||||
// Check via AIController (NPC with our behavior system)
|
||||
if (const AAIController* OtherAIC = Cast<AAIController>(OtherPawn->GetController()))
|
||||
{
|
||||
OtherTeam = OtherAIC->GetGenericTeamId().GetId();
|
||||
}
|
||||
// Check via IPS_AI_Behavior interface
|
||||
else if (OtherPawn->Implements<UPS_AI_Behavior_Interface>())
|
||||
{
|
||||
OtherTeam = IPS_AI_Behavior_Interface::Execute_GetBehaviorTeamId(const_cast<APawn*>(OtherPawn));
|
||||
}
|
||||
// Players or other pawns without AIController → NoTeam (Neutral)
|
||||
}
|
||||
|
||||
// NoTeam (255) → Neutral
|
||||
|
||||
@ -27,7 +27,6 @@
|
||||
* virtual void SetBehaviorNPCType_Implementation(EPS_AI_Behavior_NPCType T) override { MyType = T; }
|
||||
* virtual bool IsBehaviorHostile_Implementation() const override { return bHostile; }
|
||||
* virtual void SetBehaviorHostile_Implementation(bool b) override { bHostile = b; }
|
||||
* virtual uint8 GetBehaviorTeamId_Implementation() const override { return bHostile ? 2 : 1; }
|
||||
* };
|
||||
*/
|
||||
UINTERFACE(BlueprintType, Blueprintable, meta = (DisplayName = "PS AI Behavior Interface"))
|
||||
@ -69,16 +68,6 @@ public:
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
void SetBehaviorHostile(bool bNewHostile);
|
||||
|
||||
// ─── Team ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get the Team ID for perception affiliation.
|
||||
* Convention: Civilian=1, Enemy=2, Protector=3, NoTeam=255.
|
||||
* Infiltrated enemies return 1 (Civilian) until SetHostile(true).
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior")
|
||||
uint8 GetBehaviorTeamId() const;
|
||||
|
||||
// ─── Movement ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user