Connect shelter to weather protection

This commit is contained in:
2026-05-16 01:12:41 -07:00
parent 8625583faa
commit 06508061da
6 changed files with 115 additions and 3 deletions
+1 -1
View File
@@ -434,7 +434,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
- [x] Store weather source, provider timestamp, tile coordinate, and applied in-game weather state for debugging and persistence. Added tile ID/coordinate fields to mapped weather inputs, a replicated `FAgrarianWeatherDebugSnapshot` on game state, provider snapshot mapping into the debug path, and save fields for mapped inputs plus applied weather debug state. - [x] Store weather source, provider timestamp, tile coordinate, and applied in-game weather state for debugging and persistence. Added tile ID/coordinate fields to mapped weather inputs, a replicated `FAgrarianWeatherDebugSnapshot` on game state, provider snapshot mapping into the debug path, and save fields for mapped inputs plus applied weather debug state.
- [x] Add weather save/load support. Added `LoadCurrentWorld` as the unified persistence load path, restored weather/time before players and world actors, updated the admin load command to use the combined path, and extended the persistence smoke test to prove provider-backed weather metadata survives save/load. - [x] Add weather save/load support. Added `LoadCurrentWorld` as the unified persistence load path, restored weather/time before players and world actors, updated the admin load command to use the combined path, and extended the persistence smoke test to prove provider-backed weather metadata survives save/load.
- [x] Connect weather to body temperature. - [x] Connect weather to body temperature.
- [~] Connect shelter to weather protection. - [x] Connect shelter to weather protection. Survival now calculates the best overlapping shelter protection volume, replicates current weather protection, reduces ambient exposure and cold damage by shelter coverage, trends care-history shelter quality toward active protection, and shows shelter protection on the dev HUD.
- [ ] Add first-pass sky and lighting. - [ ] Add first-pass sky and lighting.
- [ ] Add audio cues for weather. - [ ] Add audio cues for weather.
+8
View File
@@ -147,6 +147,14 @@ clients to call public weather APIs directly. This keeps real-world temperature
and weather tied to the represented map tile while preserving a deterministic and weather tied to the represented map tile while preserving a deterministic
fallback if an external provider is unavailable. fallback if an external provider is unavailable.
Primitive shelters expose a replicated protection volume and
`WeatherProtection` rating. Server-side survival ticks calculate the best
overlapping shelter protection for each character, replicate the current
protection value, reduce ambient weather exposure and cold damage by that
percentage, and trend the care-history shelter quality field toward the active
protection level. The dev HUD shows current shelter protection so weather
pressure can be tuned during MVP tests.
The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses
Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the
current temperature, daily low/high, precipitation, wind, humidity, cloud cover, current temperature, daily low/high, precipitation, wind, humidity, cloud cover,
@@ -0,0 +1,64 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SURVIVAL_H = ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.h"
SURVIVAL_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianSurvivalComponent.cpp"
SHELTER_H = ROOT / "Source" / "AgrarianGame" / "AgrarianShelterActor.h"
SHELTER_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianShelterActor.cpp"
DEBUG_HUD_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianDebugHUD.cpp"
TDD = ROOT / "Docs" / "TechnicalDesignDocument.md"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
EXPECTED = {
SHELTER_H: [
"TObjectPtr<UBoxComponent> ProtectionVolume;",
"float WeatherProtection = 0.65f;",
],
SHELTER_CPP: [
"ProtectionVolume->SetBoxExtent",
"ProtectionVolume->SetCollisionProfileName(TEXT(\"OverlapAllDynamic\"))",
],
SURVIVAL_H: [
"float CurrentWeatherProtection = 0.0f;",
"float CalculateCurrentWeatherProtection() const;",
],
SURVIVAL_CPP: [
"#include \"AgrarianShelterActor.h\"",
"DOREPLIFETIME(UAgrarianSurvivalComponent, CurrentWeatherProtection);",
"CurrentWeatherProtection = CalculateCurrentWeatherProtection();",
"CareHistory.ShelterQuality = FMath::FInterpTo",
"ExposureProtectionMultiplier",
"ColdDamagePerMinute * Minutes * (1.0f - FMath::Clamp(CurrentWeatherProtection",
"Owner->GetOverlappingActors(OverlappingShelterActors, AAgrarianShelterActor::StaticClass());",
"Shelter->ProtectionVolume->IsOverlappingActor(Owner)",
],
DEBUG_HUD_CPP: [
"Shelter %3.0f%%",
"Shelter: %.0f%%",
],
TDD: [
"Primitive shelters expose a replicated protection volume",
"reduce ambient weather exposure and cold damage",
],
ROADMAP: [
"[x] Connect shelter to weather protection.",
],
}
def main() -> None:
missing = []
for path, snippets in EXPECTED.items():
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
missing.append(f"{path.relative_to(ROOT)}: {snippet}")
if missing:
raise RuntimeError("Shelter weather protection verification failed: " + "; ".join(missing))
print("Agrarian shelter weather protection verification complete.")
if __name__ == "__main__":
main()
+2
View File
@@ -102,6 +102,7 @@ void AAgrarianDebugHUD::DrawCriticalStats(const UAgrarianSurvivalComponent* Surv
DrawScaledLine(FString::Printf(TEXT("Food %3.0f"), Survival.Hunger), X, Y, CriticalStatsTextScale, StatusColor(Survival.Hunger)); DrawScaledLine(FString::Printf(TEXT("Food %3.0f"), Survival.Hunger), X, Y, CriticalStatsTextScale, StatusColor(Survival.Hunger));
DrawScaledLine(FString::Printf(TEXT("Water %3.0f"), Survival.Thirst), X, Y, CriticalStatsTextScale, StatusColor(Survival.Thirst)); DrawScaledLine(FString::Printf(TEXT("Water %3.0f"), Survival.Thirst), X, Y, CriticalStatsTextScale, StatusColor(Survival.Thirst));
DrawScaledLine(FString::Printf(TEXT("Temp %4.1f C"), Survival.BodyTemperature), X, Y, CriticalStatsTextScale, Survival.BodyTemperature < 35.0f ? CriticalColor : StableColor); DrawScaledLine(FString::Printf(TEXT("Temp %4.1f C"), Survival.BodyTemperature), X, Y, CriticalStatsTextScale, Survival.BodyTemperature < 35.0f ? CriticalColor : StableColor);
DrawScaledLine(FString::Printf(TEXT("Shelter %3.0f%%"), SurvivalComponent->CurrentWeatherProtection * 100.0f), X, Y, CriticalStatsTextScale, SurvivalComponent->CurrentWeatherProtection > 0.0f ? StableColor : WarningColor);
DrawScaledLine(FString::Printf(TEXT("Exhaust %3.0f"), Survival.Exhaustion), X, Y, CriticalStatsTextScale, StatusColor(Survival.Exhaustion, true)); DrawScaledLine(FString::Printf(TEXT("Exhaust %3.0f"), Survival.Exhaustion), X, Y, CriticalStatsTextScale, StatusColor(Survival.Exhaustion, true));
DrawScaledLine(FString::Printf(TEXT("Injury %3.0f"), Survival.InjurySeverity), X, Y, CriticalStatsTextScale, StatusColor(Survival.InjurySeverity, true)); DrawScaledLine(FString::Printf(TEXT("Injury %3.0f"), Survival.InjurySeverity), X, Y, CriticalStatsTextScale, StatusColor(Survival.InjurySeverity, true));
DrawScaledLine(FString::Printf(TEXT("Sickness %3.0f"), Survival.SicknessSeverity), X, Y, CriticalStatsTextScale, StatusColor(Survival.SicknessSeverity, true)); DrawScaledLine(FString::Printf(TEXT("Sickness %3.0f"), Survival.SicknessSeverity), X, Y, CriticalStatsTextScale, StatusColor(Survival.SicknessSeverity, true));
@@ -171,6 +172,7 @@ void AAgrarianDebugHUD::DrawSurvival(const UAgrarianSurvivalComponent* SurvivalC
DrawLine(FString::Printf(TEXT("Hunger: %.0f"), Survival.Hunger), X, Y); DrawLine(FString::Printf(TEXT("Hunger: %.0f"), Survival.Hunger), X, Y);
DrawLine(FString::Printf(TEXT("Thirst: %.0f"), Survival.Thirst), 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("Temp: %.1f C"), Survival.BodyTemperature), X, Y);
DrawLine(FString::Printf(TEXT("Shelter: %.0f%%"), SurvivalComponent->CurrentWeatherProtection * 100.0f), X, Y);
DrawLine(FString::Printf(TEXT("Injury: %.0f"), Survival.InjurySeverity), X, Y); DrawLine(FString::Printf(TEXT("Injury: %.0f"), Survival.InjurySeverity), X, Y);
DrawLine(FString::Printf(TEXT("Sick: %.0f"), Survival.SicknessSeverity), X, Y); DrawLine(FString::Printf(TEXT("Sick: %.0f"), Survival.SicknessSeverity), X, Y);
const FAgrarianCareHistorySnapshot& Care = SurvivalComponent->CareHistory; const FAgrarianCareHistorySnapshot& Care = SurvivalComponent->CareHistory;
@@ -2,6 +2,8 @@
#include "AgrarianSurvivalComponent.h" #include "AgrarianSurvivalComponent.h"
#include "AgrarianGameState.h" #include "AgrarianGameState.h"
#include "AgrarianShelterActor.h"
#include "Components/BoxComponent.h"
#include "Engine/World.h" #include "Engine/World.h"
#include "Net/UnrealNetwork.h" #include "Net/UnrealNetwork.h"
@@ -32,6 +34,8 @@ void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickT
Survival.Hunger -= HungerDecayPerMinute * Minutes; Survival.Hunger -= HungerDecayPerMinute * Minutes;
Survival.Thirst -= ThirstDecayPerMinute * Minutes; Survival.Thirst -= ThirstDecayPerMinute * Minutes;
Survival.Stamina += StaminaRecoveryPerSecond * DeltaTime; Survival.Stamina += StaminaRecoveryPerSecond * DeltaTime;
CurrentWeatherProtection = CalculateCurrentWeatherProtection();
CareHistory.ShelterQuality = FMath::FInterpTo(CareHistory.ShelterQuality, CurrentWeatherProtection, DeltaTime, 0.02f);
if (Survival.Stamina <= LowStaminaExhaustionThreshold) if (Survival.Stamina <= LowStaminaExhaustionThreshold)
{ {
@@ -57,7 +61,8 @@ void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickT
{ {
if (const AAgrarianGameState* AgrarianGameState = World->GetGameState<AAgrarianGameState>()) if (const AAgrarianGameState* AgrarianGameState = World->GetGameState<AAgrarianGameState>())
{ {
const float ExposureDelta = (AgrarianGameState->AmbientTemperatureC - 18.0f) * 0.002f * DeltaTime; const float ExposureProtectionMultiplier = 1.0f - FMath::Clamp(CurrentWeatherProtection, 0.0f, 1.0f);
const float ExposureDelta = (AgrarianGameState->AmbientTemperatureC - 18.0f) * 0.002f * DeltaTime * ExposureProtectionMultiplier;
Survival.BodyTemperature += FMath::Clamp(ExposureDelta, -0.035f, 0.02f); Survival.BodyTemperature += FMath::Clamp(ExposureDelta, -0.035f, 0.02f);
} }
} }
@@ -74,7 +79,7 @@ void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickT
if (Survival.BodyTemperature < 35.0f) if (Survival.BodyTemperature < 35.0f)
{ {
Survival.Health -= ColdDamagePerMinute * Minutes; Survival.Health -= ColdDamagePerMinute * Minutes * (1.0f - FMath::Clamp(CurrentWeatherProtection, 0.0f, 1.0f));
} }
if (Survival.SicknessSeverity >= 60.0f) if (Survival.SicknessSeverity >= 60.0f)
@@ -92,6 +97,7 @@ void UAgrarianSurvivalComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProp
Super::GetLifetimeReplicatedProps(OutLifetimeProps); Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UAgrarianSurvivalComponent, Survival); DOREPLIFETIME(UAgrarianSurvivalComponent, Survival);
DOREPLIFETIME(UAgrarianSurvivalComponent, CareHistory); DOREPLIFETIME(UAgrarianSurvivalComponent, CareHistory);
DOREPLIFETIME(UAgrarianSurvivalComponent, CurrentWeatherProtection);
} }
bool UAgrarianSurvivalComponent::IsAlive() const bool UAgrarianSurvivalComponent::IsAlive() const
@@ -229,6 +235,32 @@ void UAgrarianSurvivalComponent::ReduceExhaustion(float Amount)
} }
} }
float UAgrarianSurvivalComponent::CalculateCurrentWeatherProtection() const
{
const AActor* Owner = GetOwner();
if (!Owner)
{
return 0.0f;
}
TArray<AActor*> OverlappingShelterActors;
Owner->GetOverlappingActors(OverlappingShelterActors, AAgrarianShelterActor::StaticClass());
float BestProtection = 0.0f;
for (const AActor* Actor : OverlappingShelterActors)
{
const AAgrarianShelterActor* Shelter = Cast<AAgrarianShelterActor>(Actor);
if (!Shelter || !Shelter->ProtectionVolume || !Shelter->ProtectionVolume->IsOverlappingActor(Owner))
{
continue;
}
BestProtection = FMath::Max(BestProtection, FMath::Clamp(Shelter->WeatherProtection, 0.0f, 1.0f));
}
return BestProtection;
}
void UAgrarianSurvivalComponent::OnRep_Survival() void UAgrarianSurvivalComponent::OnRep_Survival()
{ {
BroadcastSurvivalChanged(); BroadcastSurvivalChanged();
@@ -30,6 +30,9 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_CareHistory, Category = "Agrarian|Survival") UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_CareHistory, Category = "Agrarian|Survival")
FAgrarianCareHistorySnapshot CareHistory; FAgrarianCareHistorySnapshot CareHistory;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|Survival|Shelter")
float CurrentWeatherProtection = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0"))
float HungerDecayPerMinute = 0.55f; float HungerDecayPerMinute = 0.55f;
@@ -102,6 +105,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
void ReduceExhaustion(float Amount); void ReduceExhaustion(float Amount);
UFUNCTION(BlueprintPure, Category = "Agrarian|Survival|Shelter")
float CalculateCurrentWeatherProtection() const;
protected: protected:
UFUNCTION() UFUNCTION()
void OnRep_Survival(); void OnRep_Survival();