diff --git a/AGRARIAN_FOUNDATION_STATUS.md b/AGRARIAN_FOUNDATION_STATUS.md index 266d283..75a02a1 100644 --- a/AGRARIAN_FOUNDATION_STATUS.md +++ b/AGRARIAN_FOUNDATION_STATUS.md @@ -18,6 +18,12 @@ - [x] Character now owns survival, inventory, and crafting components. - [x] Character has server-authoritative interaction path. - [x] Git ignore and Git LFS attribute prep files added. +- [x] Item definition data asset class added. +- [x] Recipe data asset class added. +- [x] Crafting component can load recipes from data assets. +- [x] Resource nodes can use item definition assets for harvest yields. +- [x] Building placement component added. +- [x] Character now owns a building placement component. ## Next Unreal Editor Tasks @@ -36,9 +42,9 @@ ## Next C++ Foundation Tasks -- [ ] Add building placement component. -- [ ] Add simple crafting recipe defaults or data asset pipeline. -- [ ] Add item definition data asset class. +- [x] Add building placement component. +- [x] Add simple crafting recipe defaults or data asset pipeline. +- [x] Add item definition data asset class. - [ ] Add save/load capture for placed actors. - [ ] Add admin/dev console commands. - [ ] Add wildlife base actor. diff --git a/Source/AgrarianGame/AgrarianBuildingPlacementComponent.cpp b/Source/AgrarianGame/AgrarianBuildingPlacementComponent.cpp new file mode 100644 index 0000000..ce9f030 --- /dev/null +++ b/Source/AgrarianGame/AgrarianBuildingPlacementComponent.cpp @@ -0,0 +1,204 @@ +// Copyright Pacificao. All Rights Reserved. + +#include "AgrarianBuildingPlacementComponent.h" +#include "AgrarianInventoryComponent.h" +#include "GameFramework/Pawn.h" +#include "GameFramework/Controller.h" +#include "Engine/World.h" + +UAgrarianBuildingPlacementComponent::UAgrarianBuildingPlacementComponent() +{ + PrimaryComponentTick.bCanEverTick = false; + SetIsReplicatedByDefault(true); +} + +void UAgrarianBuildingPlacementComponent::SetActiveBuildable(TSubclassOf BuildClass, const TArray& Cost) +{ + ActiveBuildClass = BuildClass; + PlacementCost = Cost; +} + +bool UAgrarianBuildingPlacementComponent::GetPlacementPreview(FTransform& OutTransform, FText& FailureReason) const +{ + const APawn* OwnerPawn = Cast(GetOwner()); + if (!OwnerPawn) + { + FailureReason = FText::FromString(TEXT("Only pawns can place buildables.")); + return false; + } + + FVector ViewLocation; + FRotator ViewRotation; + if (OwnerPawn->GetController()) + { + OwnerPawn->GetController()->GetPlayerViewPoint(ViewLocation, ViewRotation); + } + else + { + ViewLocation = OwnerPawn->GetActorLocation(); + ViewRotation = OwnerPawn->GetActorRotation(); + } + + const FVector TraceEnd = ViewLocation + ViewRotation.Vector() * PlacementDistance; + FHitResult Hit; + FCollisionQueryParams Params(SCENE_QUERY_STAT(AgrarianBuildPlacementTrace), false, OwnerPawn); + + FVector PlacementLocation = TraceEnd; + if (GetWorld() && GetWorld()->LineTraceSingleByChannel(Hit, ViewLocation, TraceEnd, ECC_Visibility, Params)) + { + PlacementLocation = Hit.ImpactPoint + Hit.ImpactNormal * SurfaceOffset; + } + + PlacementLocation = SnapLocation(PlacementLocation); + const FRotator PlacementRotation(0.0f, OwnerPawn->GetActorRotation().Yaw, 0.0f); + OutTransform = FTransform(PlacementRotation, PlacementLocation); + return true; +} + +bool UAgrarianBuildingPlacementComponent::CanPlaceAtTransform(TSubclassOf BuildClass, const FTransform& PlacementTransform, FText& FailureReason) const +{ + if (!BuildClass) + { + FailureReason = FText::FromString(TEXT("No buildable is selected.")); + return false; + } + + const AActor* OwnerActor = GetOwner(); + if (!OwnerActor) + { + FailureReason = FText::FromString(TEXT("No owner is available.")); + return false; + } + + if (FVector::DistSquared(OwnerActor->GetActorLocation(), PlacementTransform.GetLocation()) > FMath::Square(PlacementDistance + 150.0f)) + { + FailureReason = FText::FromString(TEXT("Placement is too far away.")); + return false; + } + + if (!GetWorld()) + { + FailureReason = FText::FromString(TEXT("No world is available.")); + return false; + } + + FCollisionQueryParams Params(SCENE_QUERY_STAT(AgrarianBuildPlacementProbe), false, OwnerActor); + const bool bBlocked = GetWorld()->OverlapBlockingTestByChannel( + PlacementTransform.GetLocation() + FVector(0.0f, 0.0f, PlacementProbeRadius), + PlacementTransform.GetRotation(), + ECC_WorldDynamic, + FCollisionShape::MakeSphere(PlacementProbeRadius), + Params); + + if (bBlocked) + { + FailureReason = FText::FromString(TEXT("Placement area is blocked.")); + return false; + } + + return true; +} + +bool UAgrarianBuildingPlacementComponent::PlaceActiveBuildable() +{ + FTransform PlacementTransform; + FText FailureReason; + if (!GetPlacementPreview(PlacementTransform, FailureReason)) + { + FailPlacement(FailureReason); + return false; + } + + if (!GetOwner()) + { + return false; + } + + if (!GetOwner()->HasAuthority()) + { + ServerPlaceBuildable(ActiveBuildClass, PlacementTransform); + return true; + } + + ServerPlaceBuildable_Implementation(ActiveBuildClass, PlacementTransform); + return true; +} + +void UAgrarianBuildingPlacementComponent::ServerPlaceBuildable_Implementation(TSubclassOf BuildClass, FTransform PlacementTransform) +{ + FText FailureReason; + if (!CanPlaceAtTransform(BuildClass, PlacementTransform, FailureReason) || !HasPlacementCost(FailureReason)) + { + FailPlacement(FailureReason); + return; + } + + FActorSpawnParameters SpawnParams; + SpawnParams.Owner = GetOwner(); + SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding; + + AActor* PlacedActor = GetWorld()->SpawnActor(BuildClass, PlacementTransform, SpawnParams); + if (!PlacedActor) + { + FailPlacement(FText::FromString(TEXT("Buildable could not be placed."))); + return; + } + + ConsumePlacementCost(); + OnBuildPlaced.Broadcast(PlacedActor); +} + +bool UAgrarianBuildingPlacementComponent::HasPlacementCost(FText& FailureReason) const +{ + const UAgrarianInventoryComponent* Inventory = GetOwner() ? GetOwner()->FindComponentByClass() : nullptr; + if (!Inventory) + { + FailureReason = FText::FromString(TEXT("No inventory is available.")); + return false; + } + + for (const FAgrarianItemStack& Cost : PlacementCost) + { + if (!Inventory->HasItem(Cost.ItemId, Cost.Quantity)) + { + FailureReason = FText::Format( + FText::FromString(TEXT("Missing build material: {0}")), + Cost.DisplayName.IsEmpty() ? FText::FromName(Cost.ItemId) : Cost.DisplayName); + return false; + } + } + + return true; +} + +void UAgrarianBuildingPlacementComponent::ConsumePlacementCost() +{ + UAgrarianInventoryComponent* Inventory = GetOwner() ? GetOwner()->FindComponentByClass() : nullptr; + if (!Inventory) + { + return; + } + + for (const FAgrarianItemStack& Cost : PlacementCost) + { + Inventory->RemoveItem(Cost.ItemId, Cost.Quantity); + } +} + +void UAgrarianBuildingPlacementComponent::FailPlacement(const FText& Reason) +{ + OnBuildPlacementFailed.Broadcast(Reason); +} + +FVector UAgrarianBuildingPlacementComponent::SnapLocation(const FVector& Location) const +{ + if (!bSnapToGrid || GridSize <= 0.0f) + { + return Location; + } + + return FVector( + FMath::GridSnap(Location.X, GridSize), + FMath::GridSnap(Location.Y, GridSize), + Location.Z); +} diff --git a/Source/AgrarianGame/AgrarianBuildingPlacementComponent.h b/Source/AgrarianGame/AgrarianBuildingPlacementComponent.h new file mode 100644 index 0000000..9805d5f --- /dev/null +++ b/Source/AgrarianGame/AgrarianBuildingPlacementComponent.h @@ -0,0 +1,68 @@ +// Copyright Pacificao. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "AgrarianTypes.h" +#include "AgrarianBuildingPlacementComponent.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianBuildPlacedSignature, AActor*, PlacedActor); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianBuildPlacementFailedSignature, FText, Reason); + +UCLASS(ClassGroup = (Agrarian), BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent)) +class UAgrarianBuildingPlacementComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UAgrarianBuildingPlacementComponent(); + + UPROPERTY(BlueprintAssignable, Category = "Agrarian|Building") + FAgrarianBuildPlacedSignature OnBuildPlaced; + + UPROPERTY(BlueprintAssignable, Category = "Agrarian|Building") + FAgrarianBuildPlacementFailedSignature OnBuildPlacementFailed; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building") + TSubclassOf ActiveBuildClass; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building") + TArray PlacementCost; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building", meta = (ClampMin = "100")) + float PlacementDistance = 600.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building", meta = (ClampMin = "0")) + float PlacementProbeRadius = 75.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building", meta = (ClampMin = "0")) + float SurfaceOffset = 2.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building") + bool bSnapToGrid = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building", meta = (EditCondition = "bSnapToGrid", ClampMin = "1")) + float GridSize = 50.0f; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Building") + void SetActiveBuildable(TSubclassOf BuildClass, const TArray& Cost); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Building") + bool GetPlacementPreview(FTransform& OutTransform, FText& FailureReason) const; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Building") + bool CanPlaceAtTransform(TSubclassOf BuildClass, const FTransform& PlacementTransform, FText& FailureReason) const; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Building") + bool PlaceActiveBuildable(); + + UFUNCTION(Server, Reliable, BlueprintCallable, Category = "Agrarian|Building") + void ServerPlaceBuildable(TSubclassOf BuildClass, FTransform PlacementTransform); + +protected: + bool HasPlacementCost(FText& FailureReason) const; + void ConsumePlacementCost(); + void FailPlacement(const FText& Reason); + FVector SnapLocation(const FVector& Location) const; +}; diff --git a/Source/AgrarianGame/AgrarianCraftingComponent.cpp b/Source/AgrarianGame/AgrarianCraftingComponent.cpp index e3dd9c7..34189b9 100644 --- a/Source/AgrarianGame/AgrarianCraftingComponent.cpp +++ b/Source/AgrarianGame/AgrarianCraftingComponent.cpp @@ -2,6 +2,7 @@ #include "AgrarianCraftingComponent.h" #include "AgrarianInventoryComponent.h" +#include "AgrarianRecipeDataAsset.h" UAgrarianCraftingComponent::UAgrarianCraftingComponent() { @@ -111,6 +112,15 @@ bool UAgrarianCraftingComponent::AddKnownRecipe(const FAgrarianRecipe& Recipe) bool UAgrarianCraftingComponent::FindRecipe(FName RecipeId, FAgrarianRecipe& OutRecipe) const { + for (const UAgrarianRecipeDataAsset* RecipeAsset : KnownRecipeAssets) + { + if (RecipeAsset && RecipeAsset->Recipe.RecipeId == RecipeId) + { + OutRecipe = RecipeAsset->Recipe; + return true; + } + } + for (const FAgrarianRecipe& Recipe : KnownRecipes) { if (Recipe.RecipeId == RecipeId) @@ -128,7 +138,7 @@ UAgrarianInventoryComponent* UAgrarianCraftingComponent::GetInventory() const return GetOwner() ? GetOwner()->FindComponentByClass() : nullptr; } -void UAgrarianCraftingComponent::FailCraft(FName RecipeId, const FText& Reason) const +void UAgrarianCraftingComponent::FailCraft(FName RecipeId, const FText& Reason) { OnCraftFailed.Broadcast(RecipeId, Reason); } diff --git a/Source/AgrarianGame/AgrarianCraftingComponent.h b/Source/AgrarianGame/AgrarianCraftingComponent.h index 453575d..7bda54b 100644 --- a/Source/AgrarianGame/AgrarianCraftingComponent.h +++ b/Source/AgrarianGame/AgrarianCraftingComponent.h @@ -8,6 +8,7 @@ #include "AgrarianCraftingComponent.generated.h" class UAgrarianInventoryComponent; +class UAgrarianRecipeDataAsset; DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAgrarianCraftCompletedSignature, FName, RecipeId, const FAgrarianItemStack&, Result); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAgrarianCraftFailedSignature, FName, RecipeId, FText, Reason); @@ -29,6 +30,9 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Crafting") TArray KnownRecipes; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Crafting") + TArray> KnownRecipeAssets; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Crafting") bool CanCraft(FName RecipeId, FText& FailureReason) const; @@ -46,5 +50,5 @@ public: protected: UAgrarianInventoryComponent* GetInventory() const; - void FailCraft(FName RecipeId, const FText& Reason) const; + void FailCraft(FName RecipeId, const FText& Reason); }; diff --git a/Source/AgrarianGame/AgrarianGameCharacter.cpp b/Source/AgrarianGame/AgrarianGameCharacter.cpp index 9a26507..835b6cc 100644 --- a/Source/AgrarianGame/AgrarianGameCharacter.cpp +++ b/Source/AgrarianGame/AgrarianGameCharacter.cpp @@ -1,6 +1,7 @@ // Copyright Epic Games, Inc. All Rights Reserved. #include "AgrarianGameCharacter.h" +#include "AgrarianBuildingPlacementComponent.h" #include "AgrarianCraftingComponent.h" #include "AgrarianInteractable.h" #include "AgrarianInventoryComponent.h" @@ -53,6 +54,7 @@ AAgrarianGameCharacter::AAgrarianGameCharacter() SurvivalComponent = CreateDefaultSubobject(TEXT("SurvivalComponent")); InventoryComponent = CreateDefaultSubobject(TEXT("InventoryComponent")); CraftingComponent = CreateDefaultSubobject(TEXT("CraftingComponent")); + BuildingPlacementComponent = CreateDefaultSubobject(TEXT("BuildingPlacementComponent")); // Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character) // are set in the derived blueprint asset named ThirdPersonCharacter (to avoid direct content references in C++) diff --git a/Source/AgrarianGame/AgrarianGameCharacter.h b/Source/AgrarianGame/AgrarianGameCharacter.h index 929965d..84105c5 100644 --- a/Source/AgrarianGame/AgrarianGameCharacter.h +++ b/Source/AgrarianGame/AgrarianGameCharacter.h @@ -10,6 +10,7 @@ class USpringArmComponent; class UCameraComponent; class UInputAction; +class UAgrarianBuildingPlacementComponent; class UAgrarianCraftingComponent; class UAgrarianInventoryComponent; class UAgrarianSurvivalComponent; @@ -45,6 +46,10 @@ class AAgrarianGameCharacter : public ACharacter /** Primitive crafting component for MVP recipes and future knowledge progression. */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) UAgrarianCraftingComponent* CraftingComponent; + + /** Server-authoritative primitive building placement component. */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true")) + UAgrarianBuildingPlacementComponent* BuildingPlacementComponent; protected: @@ -135,5 +140,8 @@ public: /** Returns CraftingComponent subobject **/ FORCEINLINE UAgrarianCraftingComponent* GetCraftingComponent() const { return CraftingComponent; } + + /** Returns BuildingPlacementComponent subobject **/ + FORCEINLINE UAgrarianBuildingPlacementComponent* GetBuildingPlacementComponent() const { return BuildingPlacementComponent; } }; diff --git a/Source/AgrarianGame/AgrarianItemDefinitionAsset.cpp b/Source/AgrarianGame/AgrarianItemDefinitionAsset.cpp new file mode 100644 index 0000000..4828a9e --- /dev/null +++ b/Source/AgrarianGame/AgrarianItemDefinitionAsset.cpp @@ -0,0 +1,13 @@ +// Copyright Pacificao. All Rights Reserved. + +#include "AgrarianItemDefinitionAsset.h" + +FAgrarianItemStack UAgrarianItemDefinitionAsset::MakeStack(int32 Quantity) const +{ + FAgrarianItemStack Stack; + Stack.ItemId = Definition.ItemId; + Stack.DisplayName = Definition.DisplayName; + Stack.Quantity = FMath::Clamp(Quantity, 0, Definition.MaxStackSize); + Stack.UnitWeight = Definition.UnitWeight; + return Stack; +} diff --git a/Source/AgrarianGame/AgrarianItemDefinitionAsset.h b/Source/AgrarianGame/AgrarianItemDefinitionAsset.h new file mode 100644 index 0000000..faad5c0 --- /dev/null +++ b/Source/AgrarianGame/AgrarianItemDefinitionAsset.h @@ -0,0 +1,21 @@ +// Copyright Pacificao. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/PrimaryDataAsset.h" +#include "AgrarianTypes.h" +#include "AgrarianItemDefinitionAsset.generated.h" + +UCLASS(BlueprintType) +class UAgrarianItemDefinitionAsset : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Agrarian|Items") + FAgrarianItemDefinition Definition; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Items") + FAgrarianItemStack MakeStack(int32 Quantity) const; +}; diff --git a/Source/AgrarianGame/AgrarianRecipeDataAsset.cpp b/Source/AgrarianGame/AgrarianRecipeDataAsset.cpp new file mode 100644 index 0000000..5278791 --- /dev/null +++ b/Source/AgrarianGame/AgrarianRecipeDataAsset.cpp @@ -0,0 +1,8 @@ +// Copyright Pacificao. All Rights Reserved. + +#include "AgrarianRecipeDataAsset.h" + +FName UAgrarianRecipeDataAsset::GetRecipeId() const +{ + return Recipe.RecipeId; +} diff --git a/Source/AgrarianGame/AgrarianRecipeDataAsset.h b/Source/AgrarianGame/AgrarianRecipeDataAsset.h new file mode 100644 index 0000000..66e616c --- /dev/null +++ b/Source/AgrarianGame/AgrarianRecipeDataAsset.h @@ -0,0 +1,21 @@ +// Copyright Pacificao. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/PrimaryDataAsset.h" +#include "AgrarianTypes.h" +#include "AgrarianRecipeDataAsset.generated.h" + +UCLASS(BlueprintType) +class UAgrarianRecipeDataAsset : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Agrarian|Crafting") + FAgrarianRecipe Recipe; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Crafting") + FName GetRecipeId() const; +}; diff --git a/Source/AgrarianGame/AgrarianResourceNode.cpp b/Source/AgrarianGame/AgrarianResourceNode.cpp index b550344..9e5d1f5 100644 --- a/Source/AgrarianGame/AgrarianResourceNode.cpp +++ b/Source/AgrarianGame/AgrarianResourceNode.cpp @@ -3,6 +3,7 @@ #include "AgrarianResourceNode.h" #include "AgrarianGameCharacter.h" #include "AgrarianInventoryComponent.h" +#include "AgrarianItemDefinitionAsset.h" #include "Components/StaticMeshComponent.h" #include "Net/UnrealNetwork.h" @@ -45,9 +46,7 @@ void AAgrarianResourceNode::Interact_Implementation(AAgrarianGameCharacter* Inte if (UAgrarianInventoryComponent* Inventory = Interactor->GetInventoryComponent()) { - FAgrarianItemStack Granted = YieldItem; - Granted.Quantity = QuantityPerHarvest; - + const FAgrarianItemStack Granted = MakeYieldStack(); if (Inventory->AddItem(Granted)) { RemainingHarvests--; @@ -61,6 +60,18 @@ void AAgrarianResourceNode::OnRep_RemainingHarvests() UpdateDepletedState(); } +FAgrarianItemStack AAgrarianResourceNode::MakeYieldStack() const +{ + if (YieldItemDefinition) + { + return YieldItemDefinition->MakeStack(QuantityPerHarvest); + } + + FAgrarianItemStack Granted = YieldItem; + Granted.Quantity = QuantityPerHarvest; + return Granted; +} + void AAgrarianResourceNode::UpdateDepletedState() { if (Mesh) diff --git a/Source/AgrarianGame/AgrarianResourceNode.h b/Source/AgrarianGame/AgrarianResourceNode.h index 52de0ea..dd14a48 100644 --- a/Source/AgrarianGame/AgrarianResourceNode.h +++ b/Source/AgrarianGame/AgrarianResourceNode.h @@ -9,6 +9,7 @@ #include "AgrarianResourceNode.generated.h" class UStaticMeshComponent; +class UAgrarianItemDefinitionAsset; UCLASS(Blueprintable) class AAgrarianResourceNode : public AActor, public IAgrarianInteractable @@ -26,6 +27,9 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource") FAgrarianItemStack YieldItem; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource") + TObjectPtr YieldItemDefinition; + UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_RemainingHarvests, Category = "Agrarian|Resource", meta = (ClampMin = "0")) int32 RemainingHarvests = 5; @@ -40,5 +44,6 @@ protected: UFUNCTION() void OnRep_RemainingHarvests(); + FAgrarianItemStack MakeYieldStack() const; void UpdateDepletedState(); };