From d318e9797718c5df1f66dd87bc0d6f2917f4e9cb Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 18 May 2026 13:22:45 -0700 Subject: [PATCH] Add survival death state --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 5 +- Scripts/verify_death_state.py | 43 ++++++++++++++ Source/AgrarianGame/AgrarianDebugHUD.cpp | 2 + .../AgrarianGamePlayerController.cpp | 8 ++- .../AgrarianSurvivalComponent.cpp | 56 ++++++++++++++++++- .../AgrarianGame/AgrarianSurvivalComponent.h | 10 ++++ Source/AgrarianGame/AgrarianTypes.h | 6 ++ 7 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 Scripts/verify_death_state.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index ddea4ee..d4a0e44 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -656,7 +656,10 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe now the MVP treatment item: item use reduces injury, bleeding, and sprain severity with a small health bump, using the existing server-authoritative `AgrarianUseItem` path. -- [ ] Add death state. +- [x] Add death state. Survival now has replicated `bIsDead` and + `LastDeathReason` state, health depletion latches death, ordinary healing + cannot revive a dead character, `Revive` provides an explicit respawn/admin + hook, and debug HUD/console survival output show alive/dead state. - [ ] Add respawn rules for MVP. - [ ] Add corpse/backpack placeholder if needed. - [ ] Add replicated death feedback. diff --git a/Scripts/verify_death_state.py b/Scripts/verify_death_state.py new file mode 100644 index 0000000..35dce20 --- /dev/null +++ b/Scripts/verify_death_state.py @@ -0,0 +1,43 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def require(path: Path, snippet: str) -> None: + text = path.read_text(encoding="utf-8") + if snippet not in text: + raise SystemExit(f"{path.relative_to(ROOT)} missing {snippet!r}") + + +def main() -> None: + types = ROOT / "Source" / "AgrarianGame" / "AgrarianTypes.h" + survival_h = ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.h" + survival_cpp = ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.cpp" + hud = ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.cpp" + controller = ROOT / "Source" / "AgrarianGame" / "AgrarianGamePlayerController.cpp" + roadmap = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + + require(types, "bool bIsDead = false;") + require(types, "FName LastDeathReason = NAME_None;") + require(survival_h, "bool IsDead() const;") + require(survival_h, "void MarkDead(FName Reason);") + require(survival_h, "void Revive(float HealthAmount = 100.0f);") + require(survival_h, "void UpdateDeathState();") + require(survival_cpp, "return !Survival.bIsDead && Survival.Health > 0.0f;") + require(survival_cpp, "bool UAgrarianSurvivalComponent::IsDead() const") + require(survival_cpp, "void UAgrarianSurvivalComponent::MarkDead") + require(survival_cpp, "void UAgrarianSurvivalComponent::Revive") + require(survival_cpp, "void UAgrarianSurvivalComponent::UpdateDeathState") + require(survival_cpp, "Survival.LastDeathReason = FName(TEXT(\"health_depleted\"));") + require(controller, "Survival.bIsDead ? TEXT(\"DEAD\") : TEXT(\"ALIVE\")") + require(controller, "SurvivalComponent->Revive(100.0f);") + require(hud, "State DEAD") + require(hud, "State: %s") + require(roadmap, "[x] Add death state.") + + print("PASS: death state is present.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianDebugHUD.cpp b/Source/AgrarianGame/AgrarianDebugHUD.cpp index 03f9a13..e2bd9ff 100644 --- a/Source/AgrarianGame/AgrarianDebugHUD.cpp +++ b/Source/AgrarianGame/AgrarianDebugHUD.cpp @@ -100,6 +100,7 @@ void AAgrarianDebugHUD::DrawCriticalStats(const UAgrarianSurvivalComponent* Surv }; DrawScaledLine(TEXT("SURVIVAL"), X, Y, CriticalStatsTextScale, HeaderColor); + DrawScaledLine(Survival.bIsDead ? TEXT("State DEAD") : TEXT("State ALIVE"), X, Y, CriticalStatsTextScale, Survival.bIsDead ? CriticalColor : StableColor); DrawScaledLine(FString::Printf(TEXT("Health %3.0f"), Survival.Health), X, Y, CriticalStatsTextScale, StatusColor(Survival.Health)); DrawScaledLine(FString::Printf(TEXT("Stamina %3.0f"), Survival.Stamina), X, Y, CriticalStatsTextScale, StatusColor(Survival.Stamina)); DrawScaledLine(FString::Printf(TEXT("Food %3.0f"), Survival.Hunger), X, Y, CriticalStatsTextScale, StatusColor(Survival.Hunger)); @@ -334,6 +335,7 @@ void AAgrarianDebugHUD::DrawSurvival(const UAgrarianSurvivalComponent* SurvivalC DrawLine(FString::Printf(TEXT("Hunger: %.0f"), Survival.Hunger), X, Y); DrawLine(FString::Printf(TEXT("Thirst: %.0f"), Survival.Thirst), X, Y); DrawLine(FString::Printf(TEXT("Temp: %.1f C"), Survival.BodyTemperature), X, Y); + DrawLine(FString::Printf(TEXT("State: %s"), Survival.bIsDead ? TEXT("DEAD") : TEXT("ALIVE")), X, Y, Survival.bIsDead ? FColor::Red : FColor::Green); DrawLine(FString::Printf(TEXT("Shelter: %.0f%%"), SurvivalComponent->CurrentWeatherProtection * 100.0f), X, Y); DrawLine(FString::Printf(TEXT("Expose: x%.2f %+3.1f C"), SurvivalComponent->CurrentWeatherExposureMultiplier, SurvivalComponent->CurrentWeatherTemperatureOffsetC), X, Y); DrawLine(FString::Printf(TEXT("Injury: %.0f"), Survival.InjurySeverity), X, Y); diff --git a/Source/AgrarianGame/AgrarianGamePlayerController.cpp b/Source/AgrarianGame/AgrarianGamePlayerController.cpp index 33a9d69..0b8b3a9 100644 --- a/Source/AgrarianGame/AgrarianGamePlayerController.cpp +++ b/Source/AgrarianGame/AgrarianGamePlayerController.cpp @@ -149,7 +149,8 @@ void AAgrarianGamePlayerController::AgrarianSurvival() const FAgrarianSurvivalSnapshot& Survival = SurvivalComponent->Survival; ClientMessage(FString::Printf( - TEXT("Health %.1f | Stamina %.1f | Exhaustion %.1f | Hunger %.1f | Thirst %.1f | Temp %.1fC | Injury %.1f | Bleeding %.1f | Sprain %.1f | Sickness %.1f"), + TEXT("%s | Health %.1f | Stamina %.1f | Exhaustion %.1f | Hunger %.1f | Thirst %.1f | Temp %.1fC | Injury %.1f | Bleeding %.1f | Sprain %.1f | Sickness %.1f | Death %s"), + Survival.bIsDead ? TEXT("DEAD") : TEXT("ALIVE"), Survival.Health, Survival.Stamina, Survival.Exhaustion, @@ -159,7 +160,8 @@ void AAgrarianGamePlayerController::AgrarianSurvival() Survival.InjurySeverity, Survival.BleedingSeverity, Survival.SprainSeverity, - Survival.SicknessSeverity)); + Survival.SicknessSeverity, + *Survival.LastDeathReason.ToString())); } void AAgrarianGamePlayerController::AgrarianHeal() @@ -322,7 +324,7 @@ void AAgrarianGamePlayerController::ServerAgrarianHeal_Implementation() return; } - SurvivalComponent->RestoreHealth(100.0f); + SurvivalComponent->Revive(100.0f); SurvivalComponent->AddFood(100.0f); SurvivalComponent->AddWater(100.0f); SurvivalComponent->ReduceExhaustion(100.0f); diff --git a/Source/AgrarianGame/AgrarianSurvivalComponent.cpp b/Source/AgrarianGame/AgrarianSurvivalComponent.cpp index 17c1cb7..7f9d910 100644 --- a/Source/AgrarianGame/AgrarianSurvivalComponent.cpp +++ b/Source/AgrarianGame/AgrarianSurvivalComponent.cpp @@ -128,7 +128,12 @@ void UAgrarianSurvivalComponent::GetLifetimeReplicatedProps(TArray 0.0f; + return !Survival.bIsDead && Survival.Health > 0.0f; +} + +bool UAgrarianSurvivalComponent::IsDead() const +{ + return Survival.bIsDead || Survival.Health <= 0.0f; } void UAgrarianSurvivalComponent::ApplyDamage(float Amount) @@ -145,12 +150,42 @@ void UAgrarianSurvivalComponent::RestoreHealth(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) { + if (Survival.bIsDead) + { + return; + } + Survival.Health += FMath::Max(0.0f, Amount); ClampSurvival(); BroadcastSurvivalChanged(); } } +void UAgrarianSurvivalComponent::MarkDead(FName Reason) +{ + if (GetOwner() && GetOwner()->HasAuthority()) + { + Survival.Health = 0.0f; + Survival.Stamina = 0.0f; + Survival.bIsDead = true; + Survival.LastDeathReason = Reason.IsNone() ? FName(TEXT("unknown")) : Reason; + BroadcastSurvivalChanged(); + } +} + +void UAgrarianSurvivalComponent::Revive(float HealthAmount) +{ + if (GetOwner() && GetOwner()->HasAuthority()) + { + Survival.bIsDead = false; + Survival.LastDeathReason = NAME_None; + Survival.Health = FMath::Clamp(HealthAmount, 1.0f, 100.0f); + Survival.Stamina = FMath::Max(Survival.Stamina, 25.0f); + ClampSurvival(); + BroadcastSurvivalChanged(); + } +} + void UAgrarianSurvivalComponent::AddFood(float Amount) { if (GetOwner() && GetOwner()->HasAuthority()) @@ -429,6 +464,7 @@ void UAgrarianSurvivalComponent::ClampSurvival() Survival.BleedingSeverity = FMath::Clamp(Survival.BleedingSeverity, 0.0f, 100.0f); Survival.SprainSeverity = FMath::Clamp(Survival.SprainSeverity, 0.0f, 100.0f); Survival.SicknessSeverity = FMath::Clamp(Survival.SicknessSeverity, 0.0f, 100.0f); + UpdateDeathState(); } void UAgrarianSurvivalComponent::ClampCareHistory() @@ -447,3 +483,21 @@ void UAgrarianSurvivalComponent::BroadcastSurvivalChanged() { OnSurvivalChanged.Broadcast(Survival); } + +void UAgrarianSurvivalComponent::UpdateDeathState() +{ + if (Survival.Health <= 0.0f) + { + Survival.Health = 0.0f; + Survival.Stamina = 0.0f; + Survival.bIsDead = true; + if (Survival.LastDeathReason.IsNone()) + { + Survival.LastDeathReason = FName(TEXT("health_depleted")); + } + } + else if (!Survival.bIsDead) + { + Survival.LastDeathReason = NAME_None; + } +} diff --git a/Source/AgrarianGame/AgrarianSurvivalComponent.h b/Source/AgrarianGame/AgrarianSurvivalComponent.h index 4496c69..66d52a2 100644 --- a/Source/AgrarianGame/AgrarianSurvivalComponent.h +++ b/Source/AgrarianGame/AgrarianSurvivalComponent.h @@ -84,12 +84,21 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") bool IsAlive() const; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") + bool IsDead() const; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") void ApplyDamage(float Amount); UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") void RestoreHealth(float Amount); + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") + void MarkDead(FName Reason); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") + void Revive(float HealthAmount = 100.0f); + UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") void AddFood(float Amount); @@ -153,5 +162,6 @@ protected: void ClampSurvival(); void ClampCareHistory(); + void UpdateDeathState(); void BroadcastSurvivalChanged(); }; diff --git a/Source/AgrarianGame/AgrarianTypes.h b/Source/AgrarianGame/AgrarianTypes.h index 6559ac1..554da88 100644 --- a/Source/AgrarianGame/AgrarianTypes.h +++ b/Source/AgrarianGame/AgrarianTypes.h @@ -362,6 +362,12 @@ struct FAgrarianSurvivalSnapshot UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival") float SicknessSeverity = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival") + bool bIsDead = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival") + FName LastDeathReason = NAME_None; }; USTRUCT(BlueprintType)