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>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0e61a77346
commit
4f2c55cd2a
@@ -0,0 +1,343 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatEnemy.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "CombatAIController.h"
|
||||
#include "Components/WidgetComponent.h"
|
||||
#include "Engine/DamageEvents.h"
|
||||
#include "CombatLifeBar.h"
|
||||
#include "TimerManager.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "Animation/AnimInstance.h"
|
||||
|
||||
ACombatEnemy::ACombatEnemy()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
// bind the attack montage ended delegate
|
||||
OnAttackMontageEnded.BindUObject(this, &ACombatEnemy::AttackMontageEnded);
|
||||
|
||||
// set the AI Controller class by default
|
||||
AIControllerClass = ACombatAIController::StaticClass();
|
||||
|
||||
// use an AI Controller regardless of whether we're placed or spawned
|
||||
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
|
||||
|
||||
// ignore the controller's yaw rotation
|
||||
bUseControllerRotationYaw = false;
|
||||
|
||||
// create the life bar
|
||||
LifeBar = CreateDefaultSubobject<UWidgetComponent>(TEXT("LifeBar"));
|
||||
LifeBar->SetupAttachment(RootComponent);
|
||||
|
||||
// set the collision capsule size
|
||||
GetCapsuleComponent()->SetCapsuleSize(35.0f, 90.0f);
|
||||
|
||||
// set the character movement properties
|
||||
GetCharacterMovement()->bUseControllerDesiredRotation = true;
|
||||
|
||||
// reset HP to maximum
|
||||
CurrentHP = MaxHP;
|
||||
}
|
||||
|
||||
void ACombatEnemy::DoAIComboAttack()
|
||||
{
|
||||
// ignore if we're already playing an attack animation
|
||||
if (bIsAttacking)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// raise the attacking flag
|
||||
bIsAttacking = true;
|
||||
|
||||
// choose how many times we're going to attack
|
||||
TargetComboCount = FMath::RandRange(1, ComboSectionNames.Num() - 1);
|
||||
|
||||
// reset the attack counter
|
||||
CurrentComboAttack = 0;
|
||||
|
||||
// play the attack montage
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
const float MontageLength = AnimInstance->Montage_Play(ComboAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
|
||||
|
||||
// subscribe to montage completed and interrupted events
|
||||
if (MontageLength > 0.0f)
|
||||
{
|
||||
// set the end delegate for the montage
|
||||
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ComboAttackMontage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::DoAIChargedAttack()
|
||||
{
|
||||
// ignore if we're already playing an attack animation
|
||||
if (bIsAttacking)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// raise the attacking flag
|
||||
bIsAttacking = true;
|
||||
|
||||
// choose how many loops are we going to charge for
|
||||
TargetChargeLoops = FMath::RandRange(MinChargeLoops, MaxChargeLoops);
|
||||
|
||||
// reset the charge loop counter
|
||||
CurrentChargeLoop = 0;
|
||||
|
||||
// play the attack montage
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
const float MontageLength = AnimInstance->Montage_Play(ChargedAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
|
||||
|
||||
// subscribe to montage completed and interrupted events
|
||||
if (MontageLength > 0.0f)
|
||||
{
|
||||
// set the end delegate for the montage
|
||||
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ChargedAttackMontage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::AttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
|
||||
{
|
||||
// reset the attacking flag
|
||||
bIsAttacking = false;
|
||||
|
||||
// call the attack completed delegate so the StateTree can continue execution
|
||||
OnAttackCompleted.ExecuteIfBound();
|
||||
}
|
||||
|
||||
const FVector& ACombatEnemy::GetLastDangerLocation() const
|
||||
{
|
||||
return LastDangerLocation;
|
||||
}
|
||||
|
||||
float ACombatEnemy::GetLastDangerTime() const
|
||||
{
|
||||
return LastDangerTime;
|
||||
}
|
||||
|
||||
void ACombatEnemy::DoAttackTrace(FName DamageSourceBone)
|
||||
{
|
||||
// sweep for objects in front of the character to be hit by the attack
|
||||
TArray<FHitResult> OutHits;
|
||||
|
||||
// start at the provided socket location, sweep forward
|
||||
const FVector TraceStart = GetMesh()->GetSocketLocation(DamageSourceBone);
|
||||
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * MeleeTraceDistance);
|
||||
|
||||
// enemies only affect Pawn collision objects; they don't knock back boxes
|
||||
FCollisionObjectQueryParams ObjectParams;
|
||||
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
|
||||
|
||||
// use a sphere shape for the sweep
|
||||
FCollisionShape CollisionShape;
|
||||
CollisionShape.SetSphere(MeleeTraceRadius);
|
||||
|
||||
// ignore self
|
||||
FCollisionQueryParams QueryParams;
|
||||
QueryParams.AddIgnoredActor(this);
|
||||
|
||||
if (GetWorld()->SweepMultiByObjectType(OutHits, TraceStart, TraceEnd, FQuat::Identity, ObjectParams, CollisionShape, QueryParams))
|
||||
{
|
||||
// iterate over each object hit
|
||||
for (const FHitResult& CurrentHit : OutHits)
|
||||
{
|
||||
/** does the actor have the player tag? */
|
||||
if (CurrentHit.GetActor()->ActorHasTag(FName("Player")))
|
||||
{
|
||||
// check if the actor is damageable
|
||||
ICombatDamageable* Damageable = Cast<ICombatDamageable>(CurrentHit.GetActor());
|
||||
|
||||
if (Damageable)
|
||||
{
|
||||
// knock upwards and away from the impact normal
|
||||
const FVector Impulse = (CurrentHit.ImpactNormal * -MeleeKnockbackImpulse) + (FVector::UpVector * MeleeLaunchImpulse);
|
||||
|
||||
// pass the damage event to the actor
|
||||
Damageable->ApplyDamage(MeleeDamage, this, CurrentHit.ImpactPoint, Impulse);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::CheckCombo()
|
||||
{
|
||||
// increase the combo counter
|
||||
++CurrentComboAttack;
|
||||
|
||||
// do we still have attacks to play in this string?
|
||||
if (CurrentComboAttack < TargetComboCount)
|
||||
{
|
||||
// jump to the next attack section
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
AnimInstance->Montage_JumpToSection(ComboSectionNames[CurrentComboAttack], ComboAttackMontage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::CheckChargedAttack()
|
||||
{
|
||||
// increase the charge loop counter
|
||||
++CurrentChargeLoop;
|
||||
|
||||
// jump to either the loop or attack section of the montage depending on whether we hit the loop target
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
AnimInstance->Montage_JumpToSection(CurrentChargeLoop >= TargetChargeLoops ? ChargeAttackSection : ChargeLoopSection, ChargedAttackMontage);
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse)
|
||||
{
|
||||
|
||||
// pass the damage event to the actor
|
||||
FDamageEvent DamageEvent;
|
||||
const float ActualDamage = TakeDamage(Damage, DamageEvent, nullptr, DamageCauser);
|
||||
|
||||
// only process knockback and effects if we received nonzero damage
|
||||
if (ActualDamage > 0.0f)
|
||||
{
|
||||
// apply the knockback impulse
|
||||
GetCharacterMovement()->AddImpulse(DamageImpulse, true);
|
||||
|
||||
// is the character ragdolling?
|
||||
if (GetMesh()->IsSimulatingPhysics())
|
||||
{
|
||||
// apply an impulse to the ragdoll
|
||||
GetMesh()->AddImpulseAtLocation(DamageImpulse * GetMesh()->GetMass(), DamageLocation);
|
||||
}
|
||||
|
||||
// stop the attack montages to interrupt the attack
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
AnimInstance->Montage_Stop(0.1f, ComboAttackMontage);
|
||||
AnimInstance->Montage_Stop(0.1f, ChargedAttackMontage);
|
||||
}
|
||||
|
||||
// pass control to BP to play effects, etc.
|
||||
ReceivedDamage(ActualDamage, DamageLocation, DamageImpulse.GetSafeNormal());
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::HandleDeath()
|
||||
{
|
||||
// hide the life bar
|
||||
LifeBar->SetHiddenInGame(true);
|
||||
|
||||
// disable the collision capsule to avoid being hit again while dead
|
||||
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
|
||||
// disable character movement
|
||||
GetCharacterMovement()->DisableMovement();
|
||||
|
||||
// enable full ragdoll physics
|
||||
GetMesh()->SetSimulatePhysics(true);
|
||||
|
||||
// call the died delegate to notify any subscribers
|
||||
OnEnemyDied.Broadcast();
|
||||
|
||||
// set up the death timer
|
||||
GetWorld()->GetTimerManager().SetTimer(DeathTimer, this, &ACombatEnemy::RemoveFromLevel, DeathRemovalTime);
|
||||
}
|
||||
|
||||
void ACombatEnemy::ApplyHealing(float Healing, AActor* Healer)
|
||||
{
|
||||
// stub
|
||||
}
|
||||
|
||||
void ACombatEnemy::NotifyDanger(const FVector& DangerLocation, AActor* DangerSource)
|
||||
{
|
||||
// ensure we're being attacked by the player
|
||||
if (DangerSource && DangerSource->ActorHasTag(FName("Player")))
|
||||
{
|
||||
// save the danger location and game time
|
||||
LastDangerLocation = DangerLocation;
|
||||
LastDangerTime = GetWorld()->GetTimeSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::RemoveFromLevel()
|
||||
{
|
||||
// destroy this actor
|
||||
Destroy();
|
||||
}
|
||||
|
||||
float ACombatEnemy::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
|
||||
{
|
||||
// only process damage if the character is still alive
|
||||
if (CurrentHP <= 0.0f)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// reduce the current HP
|
||||
CurrentHP -= Damage;
|
||||
|
||||
// have we run out of HP?
|
||||
if (CurrentHP <= 0.0f)
|
||||
{
|
||||
// die
|
||||
HandleDeath();
|
||||
}
|
||||
else
|
||||
{
|
||||
// update the life bar
|
||||
LifeBarWidget->SetLifePercentage(CurrentHP / MaxHP);
|
||||
|
||||
// enable partial ragdoll physics, but keep the pelvis vertical
|
||||
GetMesh()->SetPhysicsBlendWeight(0.5f);
|
||||
GetMesh()->SetBodySimulatePhysics(PelvisBoneName, false);
|
||||
}
|
||||
|
||||
// return the received damage amount
|
||||
return Damage;
|
||||
}
|
||||
|
||||
void ACombatEnemy::Landed(const FHitResult& Hit)
|
||||
{
|
||||
Super::Landed(Hit);
|
||||
|
||||
// is the character still alive?
|
||||
if (CurrentHP >= 0.0f)
|
||||
{
|
||||
// disable ragdoll physics
|
||||
GetMesh()->SetPhysicsBlendWeight(0.0f);
|
||||
}
|
||||
|
||||
// call the landed Delegate for StateTree
|
||||
OnEnemyLanded.ExecuteIfBound();
|
||||
}
|
||||
|
||||
void ACombatEnemy::BeginPlay()
|
||||
{
|
||||
// reset HP to maximum
|
||||
CurrentHP = MaxHP;
|
||||
|
||||
// we top the HP before BeginPlay so StateTree picks it up at the right value
|
||||
Super::BeginPlay();
|
||||
|
||||
// get the life bar widget from the widget comp
|
||||
LifeBarWidget = Cast<UCombatLifeBar>(LifeBar->GetUserWidgetObject());
|
||||
check(LifeBarWidget);
|
||||
|
||||
// fill the life bar
|
||||
LifeBarWidget->SetLifePercentage(1.0f);
|
||||
}
|
||||
|
||||
void ACombatEnemy::EndPlay(EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
Super::EndPlay(EndPlayReason);
|
||||
|
||||
// clear the death timer
|
||||
GetWorld()->GetTimerManager().ClearTimer(DeathTimer);
|
||||
}
|
||||
Reference in New Issue
Block a user