// Copyright Pacificao. All Rights Reserved. #include "AgrarianBuildingPlacementComponent.h" #include "AgrarianInventoryComponent.h" #include "DrawDebugHelpers.h" #include "GameFramework/Pawn.h" #include "GameFramework/Controller.h" #include "Engine/World.h" UAgrarianBuildingPlacementComponent::UAgrarianBuildingPlacementComponent() { PrimaryComponentTick.bCanEverTick = true; 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::GetPlacementPreviewState(FTransform& OutTransform, FText& FailureReason) const { if (!GetPlacementPreview(OutTransform, FailureReason)) { return false; } return CanPlaceAtTransform(ActiveBuildClass, OutTransform, FailureReason); } 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); } void UAgrarianBuildingPlacementComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (!bShowGhostPreview || !ActiveBuildClass || !GetWorld()) { return; } FTransform PreviewTransform; FText FailureReason; const bool bCanPlace = GetPlacementPreviewState(PreviewTransform, FailureReason); const FColor PreviewColor = bCanPlace ? ValidGhostPreviewColor : InvalidGhostPreviewColor; const FVector PreviewExtent( FMath::Max(PlacementProbeRadius, 25.0f), FMath::Max(PlacementProbeRadius, 25.0f), FMath::Max(PlacementProbeRadius * 0.5f, 25.0f)); DrawDebugBox( GetWorld(), PreviewTransform.GetLocation() + FVector(0.0f, 0.0f, PreviewExtent.Z), PreviewExtent, PreviewTransform.GetRotation(), PreviewColor, false, GhostPreviewLifetimeSeconds, 0, GhostPreviewLineThickness); OnBuildPreviewUpdated.Broadcast(bCanPlace, PreviewTransform, FailureReason); } 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); }