diff --git a/AGRARIAN_FOUNDATION_STATUS.md b/AGRARIAN_FOUNDATION_STATUS.md index f5ff4af..c5772b8 100644 --- a/AGRARIAN_FOUNDATION_STATUS.md +++ b/AGRARIAN_FOUNDATION_STATUS.md @@ -28,6 +28,7 @@ - [x] Persistence subsystem can capture and restore saveable world actors. - [x] Primitive shelter actor is marked as a persistent world actor. - [x] Admin/dev console commands added to the Agrarian player controller. +- [x] Wildlife base actor added with replicated health, simple movement states, and harvesting hooks. ## Next Unreal Editor Tasks @@ -51,4 +52,4 @@ - [x] Add item definition data asset class. - [x] Add save/load capture for placed actors. - [x] Add admin/dev console commands. -- [ ] Add wildlife base actor. +- [x] Add wildlife base actor. diff --git a/Source/AgrarianGame/AgrarianTypes.h b/Source/AgrarianGame/AgrarianTypes.h index 0f11655..d738cea 100644 --- a/Source/AgrarianGame/AgrarianTypes.h +++ b/Source/AgrarianGame/AgrarianTypes.h @@ -48,6 +48,24 @@ enum class EAgrarianItemType : uint8 Currency UMETA(DisplayName = "Currency") }; +UENUM(BlueprintType) +enum class EAgrarianWildlifeDisposition : uint8 +{ + Passive UMETA(DisplayName = "Passive"), + Fleeing UMETA(DisplayName = "Fleeing"), + Aggressive UMETA(DisplayName = "Aggressive") +}; + +UENUM(BlueprintType) +enum class EAgrarianWildlifeState : uint8 +{ + Idle UMETA(DisplayName = "Idle"), + Wandering UMETA(DisplayName = "Wandering"), + Fleeing UMETA(DisplayName = "Fleeing"), + Chasing UMETA(DisplayName = "Chasing"), + Dead UMETA(DisplayName = "Dead") +}; + USTRUCT(BlueprintType) struct FAgrarianItemDefinition { diff --git a/Source/AgrarianGame/AgrarianWildlifeBase.cpp b/Source/AgrarianGame/AgrarianWildlifeBase.cpp new file mode 100644 index 0000000..9544655 --- /dev/null +++ b/Source/AgrarianGame/AgrarianWildlifeBase.cpp @@ -0,0 +1,271 @@ +// Copyright Pacificao. All Rights Reserved. + +#include "AgrarianWildlifeBase.h" +#include "AgrarianGameCharacter.h" +#include "AgrarianInventoryComponent.h" +#include "GameFramework/CharacterMovementComponent.h" +#include "Kismet/GameplayStatics.h" +#include "Net/UnrealNetwork.h" + +AAgrarianWildlifeBase::AAgrarianWildlifeBase() +{ + PrimaryActorTick.bCanEverTick = true; + bReplicates = true; + + GetCharacterMovement()->MaxWalkSpeed = WanderSpeed; + GetCharacterMovement()->bOrientRotationToMovement = true; + bUseControllerRotationYaw = false; + + DisplayName = FText::FromString(TEXT("Wildlife")); +} + +void AAgrarianWildlifeBase::BeginPlay() +{ + Super::BeginPlay(); + + SpawnLocation = GetActorLocation(); + Health = FMath::Clamp(Health, 0.0f, MaxHealth); + ChooseWanderTarget(); + BroadcastHealthChanged(); + BroadcastStateChanged(); +} + +void AAgrarianWildlifeBase::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + + if (HasAuthority()) + { + ServerThink(DeltaSeconds); + } +} + +void AAgrarianWildlifeBase::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(AAgrarianWildlifeBase, WildlifeState); + DOREPLIFETIME(AAgrarianWildlifeBase, Health); + DOREPLIFETIME(AAgrarianWildlifeBase, bHarvested); +} + +bool AAgrarianWildlifeBase::IsAlive() const +{ + return WildlifeState != EAgrarianWildlifeState::Dead && Health > 0.0f; +} + +void AAgrarianWildlifeBase::ApplyWildlifeDamage(float Amount, AActor* InstigatorActor) +{ + if (!HasAuthority() || !IsAlive()) + { + return; + } + + Health = FMath::Clamp(Health - FMath::Max(0.0f, Amount), 0.0f, MaxHealth); + FocusActor = InstigatorActor; + BroadcastHealthChanged(); + + if (Health <= 0.0f) + { + EnterDeadState(); + return; + } + + if (Disposition == EAgrarianWildlifeDisposition::Passive || Disposition == EAgrarianWildlifeDisposition::Fleeing) + { + SetWildlifeState(EAgrarianWildlifeState::Fleeing); + } + else if (Disposition == EAgrarianWildlifeDisposition::Aggressive) + { + SetWildlifeState(EAgrarianWildlifeState::Chasing); + } +} + +void AAgrarianWildlifeBase::SetWildlifeState(EAgrarianWildlifeState NewState) +{ + if (WildlifeState == NewState) + { + return; + } + + WildlifeState = NewState; + BroadcastStateChanged(); +} + +FText AAgrarianWildlifeBase::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const +{ + if (WildlifeState == EAgrarianWildlifeState::Dead) + { + return bHarvested ? FText::FromString(TEXT("Harvested")) : FText::FromString(TEXT("Harvest")); + } + + return DisplayName; +} + +bool AAgrarianWildlifeBase::CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const +{ + return Interactor && WildlifeState == EAgrarianWildlifeState::Dead && !bHarvested; +} + +void AAgrarianWildlifeBase::Interact_Implementation(AAgrarianGameCharacter* Interactor) +{ + if (HasAuthority()) + { + Harvest(Interactor); + } +} + +void AAgrarianWildlifeBase::OnRep_Health() +{ + BroadcastHealthChanged(); +} + +void AAgrarianWildlifeBase::OnRep_WildlifeState() +{ + BroadcastStateChanged(); +} + +void AAgrarianWildlifeBase::ServerThink(float DeltaSeconds) +{ + if (!IsAlive()) + { + return; + } + + DecisionTimer -= DeltaSeconds; + if (DecisionTimer <= 0.0f) + { + DecisionTimer = DecisionIntervalSeconds; + + if (Disposition == EAgrarianWildlifeDisposition::Aggressive) + { + FocusActor = FindNearestPlayer(AggroRadius); + SetWildlifeState(FocusActor ? EAgrarianWildlifeState::Chasing : EAgrarianWildlifeState::Wandering); + } + else + { + FocusActor = FindNearestPlayer(FleeRadius); + SetWildlifeState(FocusActor ? EAgrarianWildlifeState::Fleeing : EAgrarianWildlifeState::Wandering); + } + + if (WildlifeState == EAgrarianWildlifeState::Wandering && FVector::DistSquared(GetActorLocation(), CurrentMoveTarget) < FMath::Square(125.0f)) + { + ChooseWanderTarget(); + } + } + + MoveTowardTarget(); +} + +void AAgrarianWildlifeBase::ChooseWanderTarget() +{ + const FVector RandomOffset = FVector( + FMath::FRandRange(-WanderRadius, WanderRadius), + FMath::FRandRange(-WanderRadius, WanderRadius), + 0.0f); + + CurrentMoveTarget = SpawnLocation + RandomOffset; +} + +void AAgrarianWildlifeBase::MoveTowardTarget() +{ + FVector DesiredDirection = FVector::ZeroVector; + + if (WildlifeState == EAgrarianWildlifeState::Fleeing && FocusActor) + { + DesiredDirection = GetActorLocation() - FocusActor->GetActorLocation(); + GetCharacterMovement()->MaxWalkSpeed = FleeSpeed; + } + else if (WildlifeState == EAgrarianWildlifeState::Chasing && FocusActor) + { + DesiredDirection = FocusActor->GetActorLocation() - GetActorLocation(); + GetCharacterMovement()->MaxWalkSpeed = FleeSpeed; + } + else if (WildlifeState == EAgrarianWildlifeState::Wandering) + { + DesiredDirection = CurrentMoveTarget - GetActorLocation(); + GetCharacterMovement()->MaxWalkSpeed = WanderSpeed; + } + + DesiredDirection.Z = 0.0f; + if (!DesiredDirection.IsNearlyZero()) + { + AddMovementInput(DesiredDirection.GetSafeNormal()); + } +} + +void AAgrarianWildlifeBase::EnterDeadState() +{ + Health = 0.0f; + SetWildlifeState(EAgrarianWildlifeState::Dead); + GetCharacterMovement()->StopMovementImmediately(); + GetCharacterMovement()->DisableMovement(); + SetLifeSpan(0.0f); +} + +AAgrarianGameCharacter* AAgrarianWildlifeBase::FindNearestPlayer(float Radius) const +{ + UWorld* World = GetWorld(); + if (!World) + { + return nullptr; + } + + AAgrarianGameCharacter* NearestCharacter = nullptr; + float BestDistanceSq = FMath::Square(Radius); + + TArray PlayerPawns; + UGameplayStatics::GetAllActorsOfClass(World, AAgrarianGameCharacter::StaticClass(), PlayerPawns); + for (AActor* Actor : PlayerPawns) + { + AAgrarianGameCharacter* Candidate = Cast(Actor); + if (!Candidate) + { + continue; + } + + const float DistanceSq = FVector::DistSquared(GetActorLocation(), Candidate->GetActorLocation()); + if (DistanceSq < BestDistanceSq) + { + BestDistanceSq = DistanceSq; + NearestCharacter = Candidate; + } + } + + return NearestCharacter; +} + +bool AAgrarianWildlifeBase::Harvest(AAgrarianGameCharacter* Interactor) +{ + if (!Interactor || WildlifeState != EAgrarianWildlifeState::Dead || bHarvested) + { + return false; + } + + UAgrarianInventoryComponent* Inventory = Interactor->GetInventoryComponent(); + if (!Inventory) + { + return false; + } + + for (const FAgrarianItemStack& Yield : HarvestYields) + { + if (Yield.IsValidStack()) + { + Inventory->AddItem(Yield); + } + } + + bHarvested = true; + return true; +} + +void AAgrarianWildlifeBase::BroadcastHealthChanged() +{ + OnWildlifeHealthChanged.Broadcast(Health, MaxHealth); +} + +void AAgrarianWildlifeBase::BroadcastStateChanged() +{ + OnWildlifeStateChanged.Broadcast(WildlifeState); +} diff --git a/Source/AgrarianGame/AgrarianWildlifeBase.h b/Source/AgrarianGame/AgrarianWildlifeBase.h new file mode 100644 index 0000000..833f9fe --- /dev/null +++ b/Source/AgrarianGame/AgrarianWildlifeBase.h @@ -0,0 +1,116 @@ +// Copyright Pacificao. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Character.h" +#include "AgrarianInteractable.h" +#include "AgrarianTypes.h" +#include "AgrarianWildlifeBase.generated.h" + +class AAgrarianGameCharacter; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianWildlifeStateChangedSignature, EAgrarianWildlifeState, NewState); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAgrarianWildlifeHealthChangedSignature, float, Health, float, MaxHealth); + +UCLASS(Blueprintable) +class AAgrarianWildlifeBase : public ACharacter, public IAgrarianInteractable +{ + GENERATED_BODY() + +public: + AAgrarianWildlifeBase(); + + virtual void Tick(float DeltaSeconds) override; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + UPROPERTY(BlueprintAssignable, Category = "Agrarian|Wildlife") + FAgrarianWildlifeStateChangedSignature OnWildlifeStateChanged; + + UPROPERTY(BlueprintAssignable, Category = "Agrarian|Wildlife") + FAgrarianWildlifeHealthChangedSignature OnWildlifeHealthChanged; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife") + FName WildlifeId = TEXT("wildlife"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife") + FText DisplayName; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife") + EAgrarianWildlifeDisposition Disposition = EAgrarianWildlifeDisposition::Passive; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_WildlifeState, Category = "Agrarian|Wildlife") + EAgrarianWildlifeState WildlifeState = EAgrarianWildlifeState::Idle; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife", meta = (ClampMin = "1")) + float MaxHealth = 25.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_Health, Category = "Agrarian|Wildlife", meta = (ClampMin = "0")) + float Health = 25.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0")) + float WanderRadius = 1200.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0")) + float WanderSpeed = 180.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0")) + float FleeSpeed = 420.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0")) + float AggroRadius = 650.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0")) + float FleeRadius = 700.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Movement", meta = (ClampMin = "0.1")) + float DecisionIntervalSeconds = 2.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Harvest") + TArray HarvestYields; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Wildlife|Harvest") + bool bHarvested = false; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Wildlife") + bool IsAlive() const; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Wildlife") + void ApplyWildlifeDamage(float Amount, AActor* InstigatorActor); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Wildlife") + void SetWildlifeState(EAgrarianWildlifeState NewState); + + virtual FText GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const override; + virtual bool CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const override; + virtual void Interact_Implementation(AAgrarianGameCharacter* Interactor) override; + +protected: + virtual void BeginPlay() override; + + UFUNCTION() + void OnRep_Health(); + + UFUNCTION() + void OnRep_WildlifeState(); + + void ServerThink(float DeltaSeconds); + void ChooseWanderTarget(); + void MoveTowardTarget(); + void EnterDeadState(); + AAgrarianGameCharacter* FindNearestPlayer(float Radius) const; + bool Harvest(AAgrarianGameCharacter* Interactor); + void BroadcastHealthChanged(); + void BroadcastStateChanged(); + + UPROPERTY() + FVector SpawnLocation = FVector::ZeroVector; + + UPROPERTY() + FVector CurrentMoveTarget = FVector::ZeroVector; + + UPROPERTY() + TObjectPtr FocusActor; + + float DecisionTimer = 0.0f; +};