diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index d1934b3..71f444b 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -610,7 +610,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe shelter through the server-authoritative placement component. Fully modular wall-by-wall building stays deferred to `0.2.E Permanent Structures`. - [x] Create build placement mode. -- [~] Add ghost preview. +- [x] Add ghost preview. The building placement component now exposes preview + validity/transform state and draws an assetless green/red wireframe ghost + footprint at the snapped placement location for MVP development builds. - [x] Add placement validation. - [x] Add basic shelter piece. - [ ] Add wall piece if needed. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index a85f305..c2e4800 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -86,6 +86,11 @@ Early runtime systems should remain small and explicit: as inventory parts and then combined into a single placeable primitive shelter actor. Fully modular wall-by-wall construction is deferred to permanent structures so the first survival loop remains reliable. +- `UAgrarianBuildingPlacementComponent` owns the MVP placement preview. It + traces from the player view, snaps to the configured grid, validates distance + and collision, broadcasts Blueprint-readable preview state, and draws a + green/red wireframe ghost footprint in development builds so placement can be + tested before final mesh/material ghost assets exist. - Persistence layer: save/load contracts for player and world state. Blueprints can compose and expose these systems, but core replicated behavior diff --git a/Scripts/verify_building_ghost_preview.py b/Scripts/verify_building_ghost_preview.py new file mode 100644 index 0000000..460d871 --- /dev/null +++ b/Scripts/verify_building_ghost_preview.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +HEADER = ROOT / "Source" / "AgrarianGame" / "AgrarianBuildingPlacementComponent.h" +SOURCE = ROOT / "Source" / "AgrarianGame" / "AgrarianBuildingPlacementComponent.cpp" +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" +TECHNICAL_DESIGN = ROOT / "Docs" / "TechnicalDesignDocument.md" + + +def compact(path: Path) -> str: + return " ".join(path.read_text(encoding="utf-8").split()) + + +def require(path: Path, text: str) -> None: + data = compact(path) + if text not in data: + raise SystemExit(f"FAIL: {path.relative_to(ROOT)} missing required text: {text}") + + +def main() -> None: + require(HEADER, "FAgrarianBuildPreviewUpdatedSignature") + require(HEADER, "OnBuildPreviewUpdated") + require(HEADER, "bShowGhostPreview") + require(HEADER, "GetPlacementPreviewState") + require(HEADER, "TickComponent") + require(SOURCE, "DrawDebugBox") + require(SOURCE, "ValidGhostPreviewColor") + require(SOURCE, "InvalidGhostPreviewColor") + require(SOURCE, "OnBuildPreviewUpdated.Broadcast") + require(ROADMAP, "[x] Add ghost preview.") + require(TECHNICAL_DESIGN, "green/red wireframe ghost footprint") + print("PASS: building ghost preview support is present.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianBuildingPlacementComponent.cpp b/Source/AgrarianGame/AgrarianBuildingPlacementComponent.cpp index ce9f030..2c73558 100644 --- a/Source/AgrarianGame/AgrarianBuildingPlacementComponent.cpp +++ b/Source/AgrarianGame/AgrarianBuildingPlacementComponent.cpp @@ -2,13 +2,14 @@ #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 = false; + PrimaryComponentTick.bCanEverTick = true; SetIsReplicatedByDefault(true); } @@ -55,6 +56,16 @@ bool UAgrarianBuildingPlacementComponent::GetPlacementPreview(FTransform& OutTra 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) @@ -148,6 +159,38 @@ void UAgrarianBuildingPlacementComponent::ServerPlaceBuildable_Implementation(TS 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; diff --git a/Source/AgrarianGame/AgrarianBuildingPlacementComponent.h b/Source/AgrarianGame/AgrarianBuildingPlacementComponent.h index 9805d5f..f55d6bc 100644 --- a/Source/AgrarianGame/AgrarianBuildingPlacementComponent.h +++ b/Source/AgrarianGame/AgrarianBuildingPlacementComponent.h @@ -9,6 +9,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianBuildPlacedSignature, AActor*, PlacedActor); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianBuildPlacementFailedSignature, FText, Reason); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FAgrarianBuildPreviewUpdatedSignature, bool, bCanPlace, FTransform, PreviewTransform, FText, FailureReason); UCLASS(ClassGroup = (Agrarian), BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent)) class UAgrarianBuildingPlacementComponent : public UActorComponent @@ -24,6 +25,9 @@ public: UPROPERTY(BlueprintAssignable, Category = "Agrarian|Building") FAgrarianBuildPlacementFailedSignature OnBuildPlacementFailed; + UPROPERTY(BlueprintAssignable, Category = "Agrarian|Building") + FAgrarianBuildPreviewUpdatedSignature OnBuildPreviewUpdated; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building") TSubclassOf ActiveBuildClass; @@ -45,12 +49,30 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building", meta = (EditCondition = "bSnapToGrid", ClampMin = "1")) float GridSize = 50.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building|Preview") + bool bShowGhostPreview = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building|Preview", meta = (ClampMin = "0")) + float GhostPreviewLifetimeSeconds = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building|Preview", meta = (ClampMin = "0")) + float GhostPreviewLineThickness = 2.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building|Preview") + FColor ValidGhostPreviewColor = FColor(70, 220, 120); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building|Preview") + FColor InvalidGhostPreviewColor = FColor(230, 80, 65); + 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 GetPlacementPreviewState(FTransform& OutTransform, FText& FailureReason) const; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Building") bool CanPlaceAtTransform(TSubclassOf BuildClass, const FTransform& PlacementTransform, FText& FailureReason) const; @@ -60,6 +82,8 @@ public: UFUNCTION(Server, Reliable, BlueprintCallable, Category = "Agrarian|Building") void ServerPlaceBuildable(TSubclassOf BuildClass, FTransform PlacementTransform); + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + protected: bool HasPlacementCost(FText& FailureReason) const; void ConsumePlacementCost();