Add regional temperature curve
This commit is contained in:
@@ -417,7 +417,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
- [x] Set default server time scale to `4 real hours = 1 in-game day`.
|
- [x] Set default server time scale to `4 real hours = 1 in-game day`.
|
||||||
- [x] Add real local time-zone and sunrise/sunset lookup for Ground Zero by latitude/longitude. Added tile-aware solar metadata to `AAgrarianGameState`, Ground Zero Pacifica defaults, local sunrise/sunset/solar-noon/day-length approximation, replicated solar state, `IsNight` based on active tile solar bounds, and a repeatable registry-driven solar metadata generator that skips placeholder/unknown tiles.
|
- [x] Add real local time-zone and sunrise/sunset lookup for Ground Zero by latitude/longitude. Added tile-aware solar metadata to `AAgrarianGameState`, Ground Zero Pacifica defaults, local sunrise/sunset/solar-noon/day-length approximation, replicated solar state, `IsNight` based on active tile solar bounds, and a repeatable registry-driven solar metadata generator that skips placeholder/unknown tiles.
|
||||||
- [x] Add Agrarian calendar conversion helpers for days, seasons, crop cycles, livestock maturity, spoilage, and long-running tasks. Added replicated year/day and absolute-day helpers, season/day-of-season lookup, `4 real hours = 1 in-game day` conversion helpers, long-task progress calculation, active tile growing-zone profile, crop maturity fit checks against frost-free/growing-season windows, and a repeatable growing-zone metadata generator that skips placeholder/unknown tiles.
|
- [x] Add Agrarian calendar conversion helpers for days, seasons, crop cycles, livestock maturity, spoilage, and long-running tasks. Added replicated year/day and absolute-day helpers, season/day-of-season lookup, `4 real hours = 1 in-game day` conversion helpers, long-task progress calculation, active tile growing-zone profile, crop maturity fit checks against frost-free/growing-season windows, and a repeatable growing-zone metadata generator that skips placeholder/unknown tiles.
|
||||||
- [ ] Add temperature curve by time of day.
|
- [x] Add temperature curve by time of day. Added replicated regional daily low/high temperatures, a sunrise-to-afternoon warming curve, overnight cooling, observed regional temperature blending for future real-weather adapters, weather modifiers, and deterministic fallback behavior when no provider data is available.
|
||||||
- [x] Add simple weather states.
|
- [x] Add simple weather states.
|
||||||
- [x] Add clear weather.
|
- [x] Add clear weather.
|
||||||
- [x] Add rain.
|
- [x] Add rain.
|
||||||
|
|||||||
@@ -137,6 +137,16 @@ conservative Pacifica coastal profile; later regional expansion should replace
|
|||||||
or enrich these overrides with authoritative zone, climate, and temperature
|
or enrich these overrides with authoritative zone, climate, and temperature
|
||||||
datasets.
|
datasets.
|
||||||
|
|
||||||
|
Temperature is authoritative on `AAgrarianGameState`. The MVP curve uses the
|
||||||
|
active tile's sunrise and solar noon to place the daily low near sunrise and the
|
||||||
|
daily high after solar noon, then applies weather modifiers for rain, cold wind,
|
||||||
|
and storms. Regional daily low/high values provide the deterministic fallback.
|
||||||
|
When a server-side weather adapter is available, it should set observed regional
|
||||||
|
temperature and blend weight through the game-state hook rather than allowing
|
||||||
|
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.
|
||||||
|
|
||||||
## Terrain And Tile Delivery
|
## Terrain And Tile Delivery
|
||||||
|
|
||||||
### MVP Tile
|
### MVP Tile
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
GAME_STATE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianGameState.h"
|
||||||
|
GAME_STATE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianGameState.cpp"
|
||||||
|
TDD = ROOT / "Docs" / "TechnicalDesignDocument.md"
|
||||||
|
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
|
||||||
|
|
||||||
|
|
||||||
|
EXPECTED = {
|
||||||
|
GAME_STATE_H: [
|
||||||
|
"RegionalDailyLowTemperatureC",
|
||||||
|
"RegionalDailyHighTemperatureC",
|
||||||
|
"RegionalObservedTemperatureC",
|
||||||
|
"ObservedTemperatureBlend",
|
||||||
|
"bHasRegionalObservedTemperature",
|
||||||
|
"SetRegionalTemperatureProfile",
|
||||||
|
"SetRegionalObservedTemperature",
|
||||||
|
"GetClearSkyTemperatureForHour",
|
||||||
|
],
|
||||||
|
GAME_STATE_CPP: [
|
||||||
|
"GetWrappedHourDelta",
|
||||||
|
"DOREPLIFETIME(AAgrarianGameState, RegionalDailyLowTemperatureC);",
|
||||||
|
"DOREPLIFETIME(AAgrarianGameState, RegionalWeatherSource);",
|
||||||
|
"void AAgrarianGameState::SetRegionalTemperatureProfile",
|
||||||
|
"void AAgrarianGameState::SetRegionalObservedTemperature",
|
||||||
|
"float AAgrarianGameState::GetClearSkyTemperatureForHour",
|
||||||
|
"const float LowHour = bHasActiveTileSolarData ? SunriseHourLocal : 6.0f;",
|
||||||
|
"const float HighHour = bHasActiveTileSolarData ? FMath::Fmod(SolarNoonHourLocal + 3.0f, 24.0f) : 14.0f;",
|
||||||
|
"AmbientTemperatureC = FMath::Clamp(BaseTemperatureC + WeatherModifier, -80.0f, 70.0f);",
|
||||||
|
],
|
||||||
|
TDD: [
|
||||||
|
"Temperature is authoritative on `AAgrarianGameState`",
|
||||||
|
"server-side weather adapter",
|
||||||
|
"deterministic fallback",
|
||||||
|
],
|
||||||
|
ROADMAP: [
|
||||||
|
"[x] Add temperature curve by time of day.",
|
||||||
|
"observed regional temperature blending",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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("Temperature curve verification failed: " + "; ".join(missing))
|
||||||
|
print("Agrarian temperature curve verification complete.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -25,6 +25,16 @@ bool IsDayInRange(int32 DayOfYear, int32 StartDay, int32 EndDay, int32 DaysPerYe
|
|||||||
|
|
||||||
return Day >= Start || Day <= End;
|
return Day >= Start || Day <= End;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float GetWrappedHourDelta(float FromHour, float ToHour)
|
||||||
|
{
|
||||||
|
float Delta = FMath::Fmod(ToHour - FromHour, 24.0f);
|
||||||
|
if (Delta < 0.0f)
|
||||||
|
{
|
||||||
|
Delta += 24.0f;
|
||||||
|
}
|
||||||
|
return Delta;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AAgrarianGameState::AAgrarianGameState()
|
AAgrarianGameState::AAgrarianGameState()
|
||||||
@@ -82,6 +92,12 @@ void AAgrarianGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& O
|
|||||||
DOREPLIFETIME(AAgrarianGameState, WorldHours);
|
DOREPLIFETIME(AAgrarianGameState, WorldHours);
|
||||||
DOREPLIFETIME(AAgrarianGameState, Weather);
|
DOREPLIFETIME(AAgrarianGameState, Weather);
|
||||||
DOREPLIFETIME(AAgrarianGameState, AmbientTemperatureC);
|
DOREPLIFETIME(AAgrarianGameState, AmbientTemperatureC);
|
||||||
|
DOREPLIFETIME(AAgrarianGameState, RegionalDailyLowTemperatureC);
|
||||||
|
DOREPLIFETIME(AAgrarianGameState, RegionalDailyHighTemperatureC);
|
||||||
|
DOREPLIFETIME(AAgrarianGameState, RegionalObservedTemperatureC);
|
||||||
|
DOREPLIFETIME(AAgrarianGameState, ObservedTemperatureBlend);
|
||||||
|
DOREPLIFETIME(AAgrarianGameState, bHasRegionalObservedTemperature);
|
||||||
|
DOREPLIFETIME(AAgrarianGameState, RegionalWeatherSource);
|
||||||
DOREPLIFETIME(AAgrarianGameState, DaysPerAgrarianYear);
|
DOREPLIFETIME(AAgrarianGameState, DaysPerAgrarianYear);
|
||||||
DOREPLIFETIME(AAgrarianGameState, ActiveSolarTileId);
|
DOREPLIFETIME(AAgrarianGameState, ActiveSolarTileId);
|
||||||
DOREPLIFETIME(AAgrarianGameState, ActiveTileLatitude);
|
DOREPLIFETIME(AAgrarianGameState, ActiveTileLatitude);
|
||||||
@@ -118,6 +134,56 @@ void AAgrarianGameState::SetWeather(EAgrarianWeatherType NewWeather)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AAgrarianGameState::SetRegionalTemperatureProfile(float DailyLowTemperatureC, float DailyHighTemperatureC)
|
||||||
|
{
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegionalDailyLowTemperatureC = FMath::Clamp(FMath::Min(DailyLowTemperatureC, DailyHighTemperatureC), -80.0f, 70.0f);
|
||||||
|
RegionalDailyHighTemperatureC = FMath::Clamp(FMath::Max(DailyLowTemperatureC, DailyHighTemperatureC), -80.0f, 70.0f);
|
||||||
|
UpdateAmbientTemperature();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AAgrarianGameState::SetRegionalObservedTemperature(float ObservedTemperatureC, float BlendWeight, const FString& WeatherSource)
|
||||||
|
{
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegionalObservedTemperatureC = FMath::Clamp(ObservedTemperatureC, -80.0f, 70.0f);
|
||||||
|
ObservedTemperatureBlend = FMath::Clamp(BlendWeight, 0.0f, 1.0f);
|
||||||
|
bHasRegionalObservedTemperature = ObservedTemperatureBlend > 0.0f;
|
||||||
|
RegionalWeatherSource = WeatherSource.IsEmpty() ? TEXT("server_weather_adapter") : WeatherSource;
|
||||||
|
UpdateAmbientTemperature();
|
||||||
|
}
|
||||||
|
|
||||||
|
float AAgrarianGameState::GetClearSkyTemperatureForHour(float HourOfDay) const
|
||||||
|
{
|
||||||
|
const float LowTemperature = FMath::Min(RegionalDailyLowTemperatureC, RegionalDailyHighTemperatureC);
|
||||||
|
const float HighTemperature = FMath::Max(RegionalDailyLowTemperatureC, RegionalDailyHighTemperatureC);
|
||||||
|
const float LowHour = bHasActiveTileSolarData ? SunriseHourLocal : 6.0f;
|
||||||
|
const float HighHour = bHasActiveTileSolarData ? FMath::Fmod(SolarNoonHourLocal + 3.0f, 24.0f) : 14.0f;
|
||||||
|
const float NormalizedHour = FMath::Fmod(HourOfDay, 24.0f) < 0.0f
|
||||||
|
? FMath::Fmod(HourOfDay, 24.0f) + 24.0f
|
||||||
|
: FMath::Fmod(HourOfDay, 24.0f);
|
||||||
|
|
||||||
|
const float RisingDuration = FMath::Max(0.1f, GetWrappedHourDelta(LowHour, HighHour));
|
||||||
|
const float HoursSinceLow = GetWrappedHourDelta(LowHour, NormalizedHour);
|
||||||
|
if (HoursSinceLow <= RisingDuration)
|
||||||
|
{
|
||||||
|
const float Alpha = 0.5f - (0.5f * FMath::Cos(PI * (HoursSinceLow / RisingDuration)));
|
||||||
|
return FMath::Lerp(LowTemperature, HighTemperature, Alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float CoolingDuration = FMath::Max(0.1f, 24.0f - RisingDuration);
|
||||||
|
const float HoursSinceHigh = GetWrappedHourDelta(HighHour, NormalizedHour);
|
||||||
|
const float Alpha = 0.5f - (0.5f * FMath::Cos(PI * (HoursSinceHigh / CoolingDuration)));
|
||||||
|
return FMath::Lerp(HighTemperature, LowTemperature, Alpha);
|
||||||
|
}
|
||||||
|
|
||||||
bool AAgrarianGameState::ConfigureActiveSolarTile(FName TileId, float Latitude, float Longitude, const FString& TimeZoneId, float UtcOffsetHours)
|
bool AAgrarianGameState::ConfigureActiveSolarTile(FName TileId, float Latitude, float Longitude, const FString& TimeZoneId, float UtcOffsetHours)
|
||||||
{
|
{
|
||||||
if (!HasAuthority() || TileId == NAME_None)
|
if (!HasAuthority() || TileId == NAME_None)
|
||||||
@@ -380,14 +446,20 @@ void AAgrarianGameState::UpdateSolarTimes()
|
|||||||
|
|
||||||
void AAgrarianGameState::UpdateAmbientTemperature()
|
void AAgrarianGameState::UpdateAmbientTemperature()
|
||||||
{
|
{
|
||||||
const float WarmestHour = bHasActiveTileSolarData ? FMath::Fmod(SolarNoonHourLocal + 3.0f, 24.0f) : 14.0f;
|
const float ClearSkyTemperatureC = GetClearSkyTemperatureForHour(WorldHours);
|
||||||
const float DayWarmth = FMath::Sin(((WorldHours - WarmestHour) / 24.0f) * 2.0f * PI + (PI * 0.5f)) * 8.0f;
|
const float DailyMeanTemperatureC = (RegionalDailyLowTemperatureC + RegionalDailyHighTemperatureC) * 0.5f;
|
||||||
|
const float ObservedAnchoredTemperatureC = bHasRegionalObservedTemperature
|
||||||
|
? RegionalObservedTemperatureC + (ClearSkyTemperatureC - DailyMeanTemperatureC)
|
||||||
|
: ClearSkyTemperatureC;
|
||||||
|
const float BaseTemperatureC = bHasRegionalObservedTemperature
|
||||||
|
? FMath::Lerp(ClearSkyTemperatureC, ObservedAnchoredTemperatureC, ObservedTemperatureBlend)
|
||||||
|
: ClearSkyTemperatureC;
|
||||||
float WeatherModifier = 0.0f;
|
float WeatherModifier = 0.0f;
|
||||||
|
|
||||||
switch (Weather)
|
switch (Weather)
|
||||||
{
|
{
|
||||||
case EAgrarianWeatherType::Rain:
|
case EAgrarianWeatherType::Rain:
|
||||||
WeatherModifier = -3.0f;
|
WeatherModifier = -2.0f;
|
||||||
break;
|
break;
|
||||||
case EAgrarianWeatherType::ColdWind:
|
case EAgrarianWeatherType::ColdWind:
|
||||||
WeatherModifier = -8.0f;
|
WeatherModifier = -8.0f;
|
||||||
@@ -400,5 +472,5 @@ void AAgrarianGameState::UpdateAmbientTemperature()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
AmbientTemperatureC = 10.0f + DayWarmth + WeatherModifier;
|
AmbientTemperatureC = FMath::Clamp(BaseTemperatureC + WeatherModifier, -80.0f, 70.0f);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,24 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World")
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World")
|
||||||
float AmbientTemperatureC = 12.0f;
|
float AmbientTemperatureC = 12.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Temperature")
|
||||||
|
float RegionalDailyLowTemperatureC = 9.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Temperature")
|
||||||
|
float RegionalDailyHighTemperatureC = 18.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Temperature")
|
||||||
|
float RegionalObservedTemperatureC = 12.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Temperature", meta = (ClampMin = "0.0", ClampMax = "1.0"))
|
||||||
|
float ObservedTemperatureBlend = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Temperature")
|
||||||
|
bool bHasRegionalObservedTemperature = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Temperature")
|
||||||
|
FString RegionalWeatherSource = TEXT("deterministic_tile_curve");
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
|
||||||
FName ActiveSolarTileId = TEXT("gz_us_ca_pacifica_utm10n_e544_n4160");
|
FName ActiveSolarTileId = TEXT("gz_us_ca_pacifica_utm10n_e544_n4160");
|
||||||
|
|
||||||
@@ -79,6 +97,15 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|World")
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|World")
|
||||||
void SetWeather(EAgrarianWeatherType NewWeather);
|
void SetWeather(EAgrarianWeatherType NewWeather);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|World|Temperature")
|
||||||
|
void SetRegionalTemperatureProfile(float DailyLowTemperatureC, float DailyHighTemperatureC);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|World|Temperature")
|
||||||
|
void SetRegionalObservedTemperature(float ObservedTemperatureC, float BlendWeight, const FString& WeatherSource);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Temperature")
|
||||||
|
float GetClearSkyTemperatureForHour(float HourOfDay) const;
|
||||||
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|World|Tile Solar")
|
UFUNCTION(BlueprintCallable, Category = "Agrarian|World|Tile Solar")
|
||||||
bool ConfigureActiveSolarTile(FName TileId, float Latitude, float Longitude, const FString& TimeZoneId, float UtcOffsetHours);
|
bool ConfigureActiveSolarTile(FName TileId, float Latitude, float Longitude, const FString& TimeZoneId, float UtcOffsetHours);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user