diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index cf9f40d..74ef8a7 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -631,7 +631,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add shelter protection volume. - [x] Add shelter persistence. - [x] Add shelter replication. -- [ ] Add deconstruction or damage placeholder. +- [x] Add deconstruction or damage placeholder. Primitive shelters now have + replicated structure health, authority-only damage/repair/deconstruct hooks, + TakeDamage integration, depletion destroy behavior, and save/load support for + current/max structure health. ## 0.1.J Injury And Basic Survival Consequences diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 5069cc1..39de46e 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -95,6 +95,11 @@ Early runtime systems should remain small and explicit: - MVP primitive shelters use an open entrance and do not include an interactive door. Door actors, locks, ownership permissions, and modular openings are deferred to permanent structures. +- `AAgrarianShelterActor` includes a version 0.1 structure damage placeholder: + replicated current/max health, authority-only damage, repair, and + deconstruction hooks, `TakeDamage` integration, depletion destruction, and + persistent health state. This gives fire, weather, tools, and future + ownership systems a safe structure-health contract to extend. - `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 diff --git a/Scripts/verify_shelter_damage_placeholder.py b/Scripts/verify_shelter_damage_placeholder.py new file mode 100644 index 0000000..83aa821 --- /dev/null +++ b/Scripts/verify_shelter_damage_placeholder.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +HEADER = ROOT / "Source" / "AgrarianGame" / "AgrarianShelterActor.h" +SOURCE = ROOT / "Source" / "AgrarianGame" / "AgrarianShelterActor.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, "public AActor, public IAgrarianPersistentStateProvider") + require(HEADER, "MaxStructureHealth") + require(HEADER, "CurrentStructureHealth") + require(HEADER, "ApplyStructureDamage") + require(HEADER, "RepairStructure") + require(HEADER, "Deconstruct") + require(HEADER, "GetStructureHealthRatio") + require(HEADER, "IsStructureDamaged") + require(SOURCE, "DOREPLIFETIME(AAgrarianShelterActor, MaxStructureHealth)") + require(SOURCE, "DOREPLIFETIME(AAgrarianShelterActor, CurrentStructureHealth)") + require(SOURCE, "TakeDamage") + require(SOURCE, "current_structure_health") + require(SOURCE, "max_structure_health") + require(SOURCE, "Destroy()") + require(ROADMAP, "[x] Add deconstruction or damage placeholder.") + require(TECHNICAL_DESIGN, "version 0.1 structure damage placeholder") + print("PASS: shelter damage and deconstruction placeholder is present.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianShelterActor.cpp b/Source/AgrarianGame/AgrarianShelterActor.cpp index b0a6fcc..ad5b3a8 100644 --- a/Source/AgrarianGame/AgrarianShelterActor.cpp +++ b/Source/AgrarianGame/AgrarianShelterActor.cpp @@ -4,6 +4,7 @@ #include "AgrarianPersistentActorComponent.h" #include "Components/BoxComponent.h" #include "Components/StaticMeshComponent.h" +#include "Net/UnrealNetwork.h" AAgrarianShelterActor::AAgrarianShelterActor() { @@ -20,3 +21,113 @@ AAgrarianShelterActor::AAgrarianShelterActor() PersistentActorComponent = CreateDefaultSubobject(TEXT("PersistentActorComponent")); PersistentActorComponent->ActorTypeId = TEXT("primitive_shelter"); } + +void AAgrarianShelterActor::BeginPlay() +{ + Super::BeginPlay(); + ClampStructureHealth(); +} + +float AAgrarianShelterActor::TakeDamage(float DamageAmount, const FDamageEvent& DamageEvent, AController* EventInstigator, AActor* DamageCauser) +{ + const float PreviousHealth = CurrentStructureHealth; + ApplyStructureDamage(DamageAmount, DamageCauser); + return FMath::Max(0.0f, PreviousHealth - CurrentStructureHealth); +} + +void AAgrarianShelterActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + DOREPLIFETIME(AAgrarianShelterActor, MaxStructureHealth); + DOREPLIFETIME(AAgrarianShelterActor, CurrentStructureHealth); +} + +void AAgrarianShelterActor::CapturePersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent) const +{ + if (!PersistentComponent) + { + return; + } + + PersistentComponent->NumberState.Add(TEXT("max_structure_health"), MaxStructureHealth); + PersistentComponent->NumberState.Add(TEXT("current_structure_health"), CurrentStructureHealth); +} + +void AAgrarianShelterActor::ApplyPersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent) +{ + if (!HasAuthority() || !PersistentComponent) + { + return; + } + + if (const float* SavedMaxHealth = PersistentComponent->NumberState.Find(TEXT("max_structure_health"))) + { + MaxStructureHealth = FMath::Max(1.0f, *SavedMaxHealth); + } + + if (const float* SavedCurrentHealth = PersistentComponent->NumberState.Find(TEXT("current_structure_health"))) + { + CurrentStructureHealth = *SavedCurrentHealth; + } + + ClampStructureHealth(); +} + +bool AAgrarianShelterActor::ApplyStructureDamage(float DamageAmount, AActor* DamageCauser) +{ + if (!HasAuthority() || DamageAmount <= 0.0f || CurrentStructureHealth <= 0.0f) + { + return false; + } + + CurrentStructureHealth = FMath::Clamp(CurrentStructureHealth - DamageAmount, 0.0f, MaxStructureHealth); + if (CurrentStructureHealth <= 0.0f && bDestroyWhenHealthDepleted) + { + Destroy(); + } + + return true; +} + +bool AAgrarianShelterActor::RepairStructure(float RepairAmount) +{ + if (!HasAuthority() || RepairAmount <= 0.0f || CurrentStructureHealth >= MaxStructureHealth) + { + return false; + } + + CurrentStructureHealth = FMath::Clamp(CurrentStructureHealth + RepairAmount, 0.0f, MaxStructureHealth); + return true; +} + +bool AAgrarianShelterActor::Deconstruct(AActor* RequestingActor) +{ + if (!HasAuthority() || !bCanBeDeconstructed) + { + return false; + } + + Destroy(); + return true; +} + +float AAgrarianShelterActor::GetStructureHealthRatio() const +{ + return MaxStructureHealth > 0.0f ? FMath::Clamp(CurrentStructureHealth / MaxStructureHealth, 0.0f, 1.0f) : 0.0f; +} + +bool AAgrarianShelterActor::IsStructureDamaged() const +{ + return CurrentStructureHealth < MaxStructureHealth; +} + +void AAgrarianShelterActor::OnRep_StructureHealth() +{ + ClampStructureHealth(); +} + +void AAgrarianShelterActor::ClampStructureHealth() +{ + MaxStructureHealth = FMath::Max(1.0f, MaxStructureHealth); + CurrentStructureHealth = FMath::Clamp(CurrentStructureHealth, 0.0f, MaxStructureHealth); +} diff --git a/Source/AgrarianGame/AgrarianShelterActor.h b/Source/AgrarianGame/AgrarianShelterActor.h index 371371a..b8a4fad 100644 --- a/Source/AgrarianGame/AgrarianShelterActor.h +++ b/Source/AgrarianGame/AgrarianShelterActor.h @@ -4,6 +4,7 @@ #include "CoreMinimal.h" #include "GameFramework/Actor.h" +#include "AgrarianPersistentStateProvider.h" #include "AgrarianShelterActor.generated.h" class UBoxComponent; @@ -11,13 +12,19 @@ class UAgrarianPersistentActorComponent; class UStaticMeshComponent; UCLASS(Blueprintable) -class AAgrarianShelterActor : public AActor +class AAgrarianShelterActor : public AActor, public IAgrarianPersistentStateProvider { GENERATED_BODY() public: AAgrarianShelterActor(); + virtual void BeginPlay() override; + virtual float TakeDamage(float DamageAmount, const FDamageEvent& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + virtual void CapturePersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent) const override; + virtual void ApplyPersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent) override; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Shelter") TObjectPtr Mesh; @@ -29,4 +36,37 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Shelter", meta = (ClampMin = "0", ClampMax = "1")) float WeatherProtection = 0.65f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Shelter|Damage", meta = (ClampMin = "1")) + float MaxStructureHealth = 100.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_StructureHealth, Category = "Agrarian|Shelter|Damage", meta = (ClampMin = "0")) + float CurrentStructureHealth = 100.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Shelter|Damage") + bool bCanBeDeconstructed = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Shelter|Damage") + bool bDestroyWhenHealthDepleted = true; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Shelter|Damage") + bool ApplyStructureDamage(float DamageAmount, AActor* DamageCauser); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Shelter|Damage") + bool RepairStructure(float RepairAmount); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Shelter|Damage") + bool Deconstruct(AActor* RequestingActor); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Agrarian|Shelter|Damage") + float GetStructureHealthRatio() const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Agrarian|Shelter|Damage") + bool IsStructureDamaged() const; + +protected: + UFUNCTION() + void OnRep_StructureHealth(); + + void ClampStructureHealth(); };