diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 5f65ab4..eebf5bf 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -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] 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. -- [~] 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 audio cues for weather. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 3e41263..9023d1d 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -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 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 Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the current temperature, daily low/high, precipitation, wind, humidity, cloud cover, diff --git a/Scripts/verify_shelter_weather_protection.py b/Scripts/verify_shelter_weather_protection.py new file mode 100644 index 0000000..1939489 --- /dev/null +++ b/Scripts/verify_shelter_weather_protection.py @@ -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 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() diff --git a/Source/AgrarianGame/AgrarianDebugHUD.cpp b/Source/AgrarianGame/AgrarianDebugHUD.cpp index ee0e522..2936f19 100644 --- a/Source/AgrarianGame/AgrarianDebugHUD.cpp +++ b/Source/AgrarianGame/AgrarianDebugHUD.cpp @@ -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("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("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("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)); @@ -171,6 +172,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("Shelter: %.0f%%"), SurvivalComponent->CurrentWeatherProtection * 100.0f), X, Y); DrawLine(FString::Printf(TEXT("Injury: %.0f"), Survival.InjurySeverity), X, Y); DrawLine(FString::Printf(TEXT("Sick: %.0f"), Survival.SicknessSeverity), X, Y); const FAgrarianCareHistorySnapshot& Care = SurvivalComponent->CareHistory; diff --git a/Source/AgrarianGame/AgrarianSurvivalComponent.cpp b/Source/AgrarianGame/AgrarianSurvivalComponent.cpp index bce7eb6..a9a542e 100644 --- a/Source/AgrarianGame/AgrarianSurvivalComponent.cpp +++ b/Source/AgrarianGame/AgrarianSurvivalComponent.cpp @@ -2,6 +2,8 @@ #include "AgrarianSurvivalComponent.h" #include "AgrarianGameState.h" +#include "AgrarianShelterActor.h" +#include "Components/BoxComponent.h" #include "Engine/World.h" #include "Net/UnrealNetwork.h" @@ -32,6 +34,8 @@ void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickT Survival.Hunger -= HungerDecayPerMinute * Minutes; Survival.Thirst -= ThirstDecayPerMinute * Minutes; Survival.Stamina += StaminaRecoveryPerSecond * DeltaTime; + CurrentWeatherProtection = CalculateCurrentWeatherProtection(); + CareHistory.ShelterQuality = FMath::FInterpTo(CareHistory.ShelterQuality, CurrentWeatherProtection, DeltaTime, 0.02f); if (Survival.Stamina <= LowStaminaExhaustionThreshold) { @@ -57,7 +61,8 @@ void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickT { if (const AAgrarianGameState* AgrarianGameState = World->GetGameState()) { - 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); } } @@ -74,7 +79,7 @@ void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickT 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) @@ -92,6 +97,7 @@ void UAgrarianSurvivalComponent::GetLifetimeReplicatedProps(TArray OverlappingShelterActors; + Owner->GetOverlappingActors(OverlappingShelterActors, AAgrarianShelterActor::StaticClass()); + + float BestProtection = 0.0f; + for (const AActor* Actor : OverlappingShelterActors) + { + const AAgrarianShelterActor* Shelter = Cast(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() { BroadcastSurvivalChanged(); diff --git a/Source/AgrarianGame/AgrarianSurvivalComponent.h b/Source/AgrarianGame/AgrarianSurvivalComponent.h index df6c528..689b654 100644 --- a/Source/AgrarianGame/AgrarianSurvivalComponent.h +++ b/Source/AgrarianGame/AgrarianSurvivalComponent.h @@ -30,6 +30,9 @@ public: UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_CareHistory, Category = "Agrarian|Survival") FAgrarianCareHistorySnapshot CareHistory; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|Survival|Shelter") + float CurrentWeatherProtection = 0.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0")) float HungerDecayPerMinute = 0.55f; @@ -102,6 +105,9 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival") void ReduceExhaustion(float Amount); + UFUNCTION(BlueprintPure, Category = "Agrarian|Survival|Shelter") + float CalculateCurrentWeatherProtection() const; + protected: UFUNCTION() void OnRep_Survival();