diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_Attack.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_Attack.cpp index 678ee7c..88c7fb2 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_Attack.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Private/BT/PS_AI_Behavior_BTTask_Attack.cpp @@ -2,7 +2,7 @@ #include "BT/PS_AI_Behavior_BTTask_Attack.h" #include "PS_AI_Behavior_AIController.h" -#include "PS_AI_Behavior_CombatComponent.h" +#include "PS_AI_Behavior_Interface.h" #include "PS_AI_Behavior_Definitions.h" #include "BehaviorTree/BlackboardComponent.h" #include "Navigation/PathFollowingComponent.h" @@ -23,7 +23,6 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask( UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent(); if (!BB) return EBTNodeResult::Failed; - // Get threat actor AActor* Target = Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)); if (!Target) { @@ -31,46 +30,31 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::ExecuteTask( return EBTNodeResult::Failed; } - // Get combat component - UPS_AI_Behavior_CombatComponent* Combat = - AIC->GetPawn()->FindComponentByClass(); - if (!Combat) - { - UE_LOG(LogPS_AI_Behavior, Warning, - 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); - } - // Stay InProgress — keep attacking while in combat state - return EBTNodeResult::InProgress; - } - - // Out of range — move toward target - const EPathFollowingRequestResult::Type Result = AIC->MoveToActor( - Target, Combat->AttackRange * 0.8f, /*bUsePathfinding=*/true, - /*bAllowStrafe=*/true); - - if (Result == EPathFollowingRequestResult::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; - } - + APawn* Pawn = AIC->GetPawn(); FAttackMemory* Memory = reinterpret_cast(NodeMemory); - Memory->bMovingToTarget = true; + Memory->bMovingToTarget = false; + Memory->bAttacking = false; + + // Tell the Pawn to start attacking via interface + if (Pawn->Implements()) + { + IPS_AI_Behavior_Interface::Execute_BehaviorStartAttack(Pawn, Target); + Memory->bAttacking = true; + } + + // Move toward target + const EPathFollowingRequestResult::Type Result = AIC->MoveToActor( + Target, AttackMoveRadius, /*bUsePathfinding=*/true, /*bAllowStrafe=*/true); + + if (Result != EPathFollowingRequestResult::AlreadyAtGoal) + { + Memory->bMovingToTarget = true; + } + + UE_LOG(LogPS_AI_Behavior, Verbose, TEXT("[%s] Attack: started on '%s'"), + *AIC->GetName(), *Target->GetName()); + + // Stay InProgress — the Decorator Observer Aborts will pull us out return EBTNodeResult::InProgress; } @@ -88,53 +72,21 @@ void UPS_AI_Behavior_BTTask_Attack::TickTask( AActor* Target = BB ? Cast(BB->GetValueAsObject(PS_AI_Behavior_BB::ThreatActor)) : nullptr; if (!Target) { - AIC->StopMovement(); FinishLatentTask(OwnerComp, EBTNodeResult::Failed); return; } - UPS_AI_Behavior_CombatComponent* Combat = - AIC->GetPawn()->FindComponentByClass(); - if (!Combat) + FAttackMemory* Memory = reinterpret_cast(NodeMemory); + + // Keep moving toward target if out of range + if (Memory->bMovingToTarget && AIC->GetMoveStatus() == EPathFollowingStatus::Idle) { - FinishLatentTask(OwnerComp, EBTNodeResult::Failed); - return; + // Re-issue move if target moved + AIC->MoveToActor(Target, AttackMoveRadius, /*bUsePathfinding=*/true, /*bAllowStrafe=*/true); } - // Check if we can attack now - if (Combat->IsInAttackRange(Target)) - { - FAttackMemory* Memory = reinterpret_cast(NodeMemory); - if (Memory->bMovingToTarget) - { - AIC->StopMovement(); - Memory->bMovingToTarget = false; - } - - if (Combat->CanAttack()) - { - Combat->ExecuteAttack(Target); - } - // Stay InProgress — keep attacking (cooldown handles the rate) - // Observer Aborts on the Decorator will pull us out when state changes - } - else - { - // Still moving — check if movement failed - FAttackMemory* Memory = reinterpret_cast(NodeMemory); - if (Memory->bMovingToTarget && AIC->GetMoveStatus() == EPathFollowingStatus::Idle) - { - // Movement ended but not in range — try again - const EPathFollowingRequestResult::Type Result = AIC->MoveToActor( - Target, Combat->AttackRange * 0.8f, /*bUsePathfinding=*/true, - /*bAllowStrafe=*/true); - - if (Result == EPathFollowingRequestResult::Failed) - { - FinishLatentTask(OwnerComp, EBTNodeResult::Failed); - } - } - } + // The Pawn handles the actual shooting/melee via the interface + // We just keep the NPC moving toward the target } EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask( @@ -144,11 +96,18 @@ EBTNodeResult::Type UPS_AI_Behavior_BTTask_Attack::AbortTask( if (AIC) { AIC->StopMovement(); + + // Tell the Pawn to stop attacking + APawn* Pawn = AIC->GetPawn(); + if (Pawn && Pawn->Implements()) + { + IPS_AI_Behavior_Interface::Execute_BehaviorStopAttack(Pawn); + } } return EBTNodeResult::Aborted; } FString UPS_AI_Behavior_BTTask_Attack::GetStaticDescription() const { - return TEXT("Move to threat and attack via CombatComponent."); + return FString::Printf(TEXT("Move to threat (radius %.0fcm) and attack via interface."), AttackMoveRadius); } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_Attack.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_Attack.h index 74e645e..f770921 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_Attack.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/BT/PS_AI_Behavior_BTTask_Attack.h @@ -7,9 +7,14 @@ #include "PS_AI_Behavior_BTTask_Attack.generated.h" /** - * BT Task: Move to and attack the threat actor. - * If out of range, moves toward the target. If in range, executes attack via CombatComponent. - * Succeeds after one attack, fails if target is lost or unreachable. + * BT Task: Move toward the threat actor and delegate combat to the Pawn. + * + * Calls IPS_AI_Behavior_Interface::BehaviorStartAttack() on enter and + * BehaviorStopAttack() on abort. The Pawn handles the actual combat + * (aim, fire, melee, etc.) via its own systems. + * + * Stays InProgress permanently — the Decorator Observer Aborts pulls it out + * when the BehaviorState changes away from Combat. */ UCLASS(meta = (DisplayName = "PS AI: Attack")) class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Attack : public UBTTaskNode @@ -19,6 +24,10 @@ class PS_AI_BEHAVIOR_API UPS_AI_Behavior_BTTask_Attack : public UBTTaskNode public: UPS_AI_Behavior_BTTask_Attack(); + /** How close the NPC tries to get to the target (cm). */ + UPROPERTY(EditAnywhere, Category = "Attack", meta = (ClampMin = "50.0")) + float AttackMoveRadius = 300.0f; + protected: virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override; @@ -29,6 +38,7 @@ private: struct FAttackMemory { bool bMovingToTarget = false; + bool bAttacking = false; }; virtual uint16 GetInstanceMemorySize() const override { return sizeof(FAttackMemory); } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h index fa3cd11..ada564c 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Behavior/Source/PS_AI_Behavior/Public/PS_AI_Behavior_Interface.h @@ -100,4 +100,30 @@ public: */ UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior") void OnBehaviorStateChanged(EPS_AI_Behavior_State NewState, EPS_AI_Behavior_State OldState); + + // ─── Combat ───────────────────────────────────────────────────────── + + /** + * Order the Pawn to start attacking a target. + * The Pawn implements this with its own combat system (aim, fire, melee, etc.). + * Called when the BT enters the Attack task. + * + * @param Target The actor to attack. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior") + void BehaviorStartAttack(AActor* Target); + + /** + * Order the Pawn to stop attacking. + * Called when the BT exits the Attack task (state changed, target lost, etc.). + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior") + void BehaviorStopAttack(); + + /** + * Query whether the Pawn can currently attack the target (has ammo, weapon ready, etc.). + * If false, the BT will keep the NPC in combat stance but won't call BehaviorStartAttack. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS AI Behavior") + bool CanBehaviorAttack(AActor* Target) const; };