Files
Rainer LeitandClaude Fable 5.1 4f2c55cd2a Step 1: the Salty project, two modules, tests and the gym
Salty.uproject (UE 5.8) from the Third Person template with class redirects, the SaltyCore and Salty modules,
the three build targets, USaltyAssetManager calling InitGlobalData, Config/Tags with the 23 root namespaces,
Scripts/run-tests.sh, build.sh and Authoring/create_gym.py, L_Gym, Git LFS attributes and the placeholder test.
Template variants kept as reference (D-41). The four Fab packs stay out of the repository for now (~10 GB).
The editor serves the engine MCP plugin on 127.0.0.1:8000 through DefaultEditorPerProjectUserSettings.ini.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:10:43 +03:00

316 lines
11 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatStateTreeUtility.h"
#include "StateTreeExecutionContext.h"
#include "StateTreeExecutionTypes.h"
#include "Engine/World.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "AIController.h"
#include "CombatEnemy.h"
#include "Kismet/GameplayStatics.h"
#include "StateTreeAsyncExecutionContext.h"
bool FStateTreeCharacterGroundedCondition::TestCondition(FStateTreeExecutionContext& Context) const
{
const FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// is the character currently grounded?
bool bCondition = InstanceData.Character->GetMovementComponent()->IsMovingOnGround();
return InstanceData.bMustBeOnAir ? !bCondition : bCondition;
}
#if WITH_EDITOR
FText FStateTreeCharacterGroundedCondition::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Is Character Grounded</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
bool FStateTreeIsInDangerCondition::TestCondition(FStateTreeExecutionContext& Context) const
{
const FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// ensure we have a valid enemy character
if (InstanceData.Character)
{
// is the last detected danger event within the reaction threshold?
const float ReactionDelta = InstanceData.Character->GetWorld()->GetTimeSeconds() - InstanceData.Character->GetLastDangerTime();
if (ReactionDelta < InstanceData.MaxReactionTime && ReactionDelta > InstanceData.MinReactionTime)
{
// do a dot product check to determine if the danger location is within the character's detection cone
const FVector DangerDir = (InstanceData.Character->GetLastDangerLocation() - InstanceData.Character->GetActorLocation()).GetSafeNormal2D();
const float DangerDot = FVector::DotProduct(DangerDir, InstanceData.Character->GetActorForwardVector());
const float ConeAngleCos = FMath::Cos(FMath::DegreesToRadians(InstanceData.DangerSightConeAngle));
return DangerDot > ConeAngleCos;
}
}
return false;
}
#if WITH_EDITOR
FText FStateTreeIsInDangerCondition::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Is Character In Danger</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeComboAttackTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// bind to the on attack completed delegate
InstanceData.Character->OnAttackCompleted.BindLambda(
[WeakContext = Context.MakeWeakExecutionContext()]()
{
WeakContext.FinishTask(EStateTreeFinishTaskType::Succeeded);
}
);
// tell the character to do a combo attack
InstanceData.Character->DoAIComboAttack();
return EStateTreeRunStatus::Running;
}
void FStateTreeComboAttackTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// unbind the on attack completed delegate
InstanceData.Character->OnAttackCompleted.Unbind();
}
#if WITH_EDITOR
FText FStateTreeComboAttackTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Do Combo Attack</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeChargedAttackTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// bind to the on attack completed delegate
InstanceData.Character->OnAttackCompleted.BindLambda(
[WeakContext = Context.MakeWeakExecutionContext()]()
{
WeakContext.FinishTask(EStateTreeFinishTaskType::Succeeded);
}
);
// tell the character to do a charged attack
InstanceData.Character->DoAIChargedAttack();
return EStateTreeRunStatus::Running;
}
void FStateTreeChargedAttackTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// unbind the on attack completed delegate
InstanceData.Character->OnAttackCompleted.Unbind();
}
#if WITH_EDITOR
FText FStateTreeChargedAttackTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Do Charged Attack</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeWaitForLandingTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// bind to the on enemy landed delegate
InstanceData.Character->OnEnemyLanded.BindLambda(
[WeakContext = Context.MakeWeakExecutionContext()]()
{
WeakContext.FinishTask(EStateTreeFinishTaskType::Succeeded);
}
);
return EStateTreeRunStatus::Running;
}
void FStateTreeWaitForLandingTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// unbind the on enemy landed delegate
InstanceData.Character->OnEnemyLanded.Unbind();
}
#if WITH_EDITOR
FText FStateTreeWaitForLandingTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Wait for Landing</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeFaceActorTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// set the AI Controller's focus
InstanceData.Controller->SetFocus(InstanceData.ActorToFaceTowards);
return EStateTreeRunStatus::Running;
}
void FStateTreeFaceActorTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// clear the AI Controller's focus
InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay);
}
#if WITH_EDITOR
FText FStateTreeFaceActorTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Face Towards Actor</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeFaceLocationTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// set the AI Controller's focus
InstanceData.Controller->SetFocalPoint(InstanceData.FaceLocation);
return EStateTreeRunStatus::Running;
}
void FStateTreeFaceLocationTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// clear the AI Controller's focus
InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay);
}
#if WITH_EDITOR
FText FStateTreeFaceLocationTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Face Towards Location</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeSetCharacterSpeedTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// set the character's max ground speed
InstanceData.Character->GetCharacterMovement()->MaxWalkSpeed = InstanceData.Speed;
return EStateTreeRunStatus::Running;
}
#if WITH_EDITOR
FText FStateTreeSetCharacterSpeedTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Set Character Speed</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeGetPlayerInfoTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// reset the selected target
ACharacter* SelectedTarget = nullptr;
// iterate through each local player
const int32 NumPlayers = UGameplayStatics::GetNumLocalPlayerControllers(InstanceData.Character);
for (int32 i = 0; i < NumPlayers; ++i)
{
if (ACharacter* Current = Cast<ACharacter>(UGameplayStatics::GetPlayerPawn(InstanceData.Character, i)))
{
// compute the distance to the target
const float TargetDist = (Current->GetActorLocation() - InstanceData.Character->GetActorLocation()).Size();
// is this target within range?
if (TargetDist < InstanceData.MaxRange)
{
// have we selected a valid target already?
if (SelectedTarget)
{
// randomly switch to the new target
if (FMath::RandBool())
{
SelectedTarget = Current;
}
}
else
{
// no valid target yet, so choose this one
SelectedTarget = Current;
}
}
}
}
// set the new target
InstanceData.TargetPlayerCharacter = SelectedTarget;
// if the target is not valid, fail the task
if (!SelectedTarget)
{
return EStateTreeRunStatus::Failed;
}
// set the target location and distance
InstanceData.TargetPlayerLocation = SelectedTarget->GetActorLocation();
InstanceData.DistanceToTarget = (SelectedTarget->GetActorLocation() - InstanceData.Character->GetActorLocation()).Size();
// succeed
return EStateTreeRunStatus::Succeeded;
}
#if WITH_EDITOR
FText FStateTreeGetPlayerInfoTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Get Player Info</b>");
}
#endif // WITH_EDITOR