540 lines
20 KiB
C++
540 lines
20 KiB
C++
// Copyright Pacificao. All Rights Reserved.
|
|
|
|
#include "AgrarianGameState.h"
|
|
#include "AgrarianPerformanceStats.h"
|
|
#include "Net/UnrealNetwork.h"
|
|
#include "ProfilingDebugging/CpuProfilerTrace.h"
|
|
|
|
namespace
|
|
{
|
|
int32 NormalizeCalendarDay(int32 DayOfYear, int32 DaysPerYear)
|
|
{
|
|
const int32 SafeDaysPerYear = FMath::Max(1, DaysPerYear);
|
|
const int32 ZeroBasedDay = ((DayOfYear - 1) % SafeDaysPerYear + SafeDaysPerYear) % SafeDaysPerYear;
|
|
return ZeroBasedDay + 1;
|
|
}
|
|
|
|
bool IsDayInRange(int32 DayOfYear, int32 StartDay, int32 EndDay, int32 DaysPerYear)
|
|
{
|
|
const int32 Day = NormalizeCalendarDay(DayOfYear, DaysPerYear);
|
|
const int32 Start = NormalizeCalendarDay(StartDay, DaysPerYear);
|
|
const int32 End = NormalizeCalendarDay(EndDay, DaysPerYear);
|
|
|
|
if (Start <= End)
|
|
{
|
|
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()
|
|
{
|
|
PrimaryActorTick.bCanEverTick = true;
|
|
bReplicates = true;
|
|
ActiveGrowingSeason.TileId = ActiveSolarTileId;
|
|
ActiveGrowingSeason.GrowingZoneLabel = TEXT("USDA 10a");
|
|
ActiveGrowingSeason.GrowingSeasonStartDay = 46;
|
|
ActiveGrowingSeason.GrowingSeasonEndDay = 350;
|
|
ActiveGrowingSeason.FrostFreeDays = 305;
|
|
ActiveGrowingSeason.MinAverageGrowingTempC = 7.0f;
|
|
ActiveGrowingSeason.CropSafetyBufferDays = 14;
|
|
ActiveGrowingSeason.ClimateProfile = TEXT("coastal_mediterranean_mild");
|
|
ActiveWeatherInputs.TileId = ActiveSolarTileId;
|
|
ActiveWeatherInputs.Latitude = ActiveTileLatitude;
|
|
ActiveWeatherInputs.Longitude = ActiveTileLongitude;
|
|
ActiveWeatherDebug.TileId = ActiveSolarTileId;
|
|
ActiveWeatherDebug.Latitude = ActiveTileLatitude;
|
|
ActiveWeatherDebug.Longitude = ActiveTileLongitude;
|
|
ActiveWeatherDebug.AppliedWeather = Weather;
|
|
}
|
|
|
|
void AAgrarianGameState::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
if (HasAuthority())
|
|
{
|
|
UpdateSolarTimes();
|
|
}
|
|
}
|
|
|
|
void AAgrarianGameState::Tick(float DeltaSeconds)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_AgrarianGameStateTick);
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianGameStateTick);
|
|
|
|
Super::Tick(DeltaSeconds);
|
|
|
|
if (!HasAuthority())
|
|
{
|
|
return;
|
|
}
|
|
|
|
WorldHours += (DeltaSeconds / 60.0f) * GameHoursPerRealMinute;
|
|
while (WorldHours >= 24.0f)
|
|
{
|
|
WorldHours -= 24.0f;
|
|
const int32 PreviousDayOfYear = ActiveDayOfYear;
|
|
ActiveDayOfYear = (ActiveDayOfYear % FMath::Max(1, DaysPerAgrarianYear)) + 1;
|
|
if (ActiveDayOfYear < PreviousDayOfYear)
|
|
{
|
|
++ActiveYear;
|
|
}
|
|
UpdateSolarTimes();
|
|
}
|
|
|
|
UpdateAmbientTemperature();
|
|
}
|
|
|
|
void AAgrarianGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
|
{
|
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
|
DOREPLIFETIME(AAgrarianGameState, WorldHours);
|
|
DOREPLIFETIME(AAgrarianGameState, Weather);
|
|
DOREPLIFETIME(AAgrarianGameState, AmbientTemperatureC);
|
|
DOREPLIFETIME(AAgrarianGameState, RegionalDailyLowTemperatureC);
|
|
DOREPLIFETIME(AAgrarianGameState, RegionalDailyHighTemperatureC);
|
|
DOREPLIFETIME(AAgrarianGameState, RegionalObservedTemperatureC);
|
|
DOREPLIFETIME(AAgrarianGameState, ObservedTemperatureBlend);
|
|
DOREPLIFETIME(AAgrarianGameState, bHasRegionalObservedTemperature);
|
|
DOREPLIFETIME(AAgrarianGameState, RegionalWeatherSource);
|
|
DOREPLIFETIME(AAgrarianGameState, ActiveWeatherInputs);
|
|
DOREPLIFETIME(AAgrarianGameState, ActiveWeatherDebug);
|
|
DOREPLIFETIME(AAgrarianGameState, DaysPerAgrarianYear);
|
|
DOREPLIFETIME(AAgrarianGameState, ActiveSolarTileId);
|
|
DOREPLIFETIME(AAgrarianGameState, ActiveTileLatitude);
|
|
DOREPLIFETIME(AAgrarianGameState, ActiveTileLongitude);
|
|
DOREPLIFETIME(AAgrarianGameState, ActiveTileTimeZoneId);
|
|
DOREPLIFETIME(AAgrarianGameState, ActiveTileUtcOffsetHours);
|
|
DOREPLIFETIME(AAgrarianGameState, ActiveDayOfYear);
|
|
DOREPLIFETIME(AAgrarianGameState, ActiveYear);
|
|
DOREPLIFETIME(AAgrarianGameState, ActiveGrowingSeason);
|
|
DOREPLIFETIME(AAgrarianGameState, bHasActiveTileSolarData);
|
|
DOREPLIFETIME(AAgrarianGameState, SunriseHourLocal);
|
|
DOREPLIFETIME(AAgrarianGameState, SunsetHourLocal);
|
|
DOREPLIFETIME(AAgrarianGameState, SolarNoonHourLocal);
|
|
DOREPLIFETIME(AAgrarianGameState, DayLengthHours);
|
|
}
|
|
|
|
bool AAgrarianGameState::IsNight() const
|
|
{
|
|
if (bHasActiveTileSolarData)
|
|
{
|
|
return WorldHours < SunriseHourLocal || WorldHours > SunsetHourLocal;
|
|
}
|
|
|
|
return WorldHours < 6.0f || WorldHours > 20.0f;
|
|
}
|
|
|
|
void AAgrarianGameState::SetWeather(EAgrarianWeatherType NewWeather)
|
|
{
|
|
if (HasAuthority())
|
|
{
|
|
Weather = NewWeather;
|
|
ActiveWeatherDebug.AppliedWeather = NewWeather;
|
|
UpdateAmbientTemperature();
|
|
OnRep_Weather();
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
void AAgrarianGameState::ApplyMappedWeatherInputs(const FAgrarianMappedWeatherInputs& MappedInputs)
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
return;
|
|
}
|
|
|
|
ActiveWeatherInputs = MappedInputs;
|
|
ActiveWeatherInputs.TemperatureC = FMath::Clamp(ActiveWeatherInputs.TemperatureC, -80.0f, 70.0f);
|
|
ActiveWeatherInputs.DailyLowTemperatureC = FMath::Clamp(ActiveWeatherInputs.DailyLowTemperatureC, -80.0f, 70.0f);
|
|
ActiveWeatherInputs.DailyHighTemperatureC = FMath::Clamp(ActiveWeatherInputs.DailyHighTemperatureC, -80.0f, 70.0f);
|
|
ActiveWeatherInputs.PrecipitationMm = FMath::Max(0.0f, ActiveWeatherInputs.PrecipitationMm);
|
|
ActiveWeatherInputs.WindSpeedKmh = FMath::Max(0.0f, ActiveWeatherInputs.WindSpeedKmh);
|
|
ActiveWeatherInputs.CloudCoverPercent = FMath::Clamp(ActiveWeatherInputs.CloudCoverPercent, 0.0f, 100.0f);
|
|
ActiveWeatherInputs.RelativeHumidityPercent = FMath::Clamp(ActiveWeatherInputs.RelativeHumidityPercent, 0.0f, 100.0f);
|
|
ActiveWeatherInputs.PressureMslHpa = FMath::Clamp(ActiveWeatherInputs.PressureMslHpa, 800.0f, 1100.0f);
|
|
ActiveWeatherInputs.VisibilityMeters = FMath::Max(0.0f, ActiveWeatherInputs.VisibilityMeters);
|
|
ActiveWeatherInputs.bHasProviderData = true;
|
|
|
|
ActiveWeatherDebug.TileId = ActiveWeatherInputs.TileId != NAME_None ? ActiveWeatherInputs.TileId : ActiveSolarTileId;
|
|
ActiveWeatherDebug.Latitude = ActiveWeatherInputs.TileId != NAME_None ? ActiveWeatherInputs.Latitude : ActiveTileLatitude;
|
|
ActiveWeatherDebug.Longitude = ActiveWeatherInputs.TileId != NAME_None ? ActiveWeatherInputs.Longitude : ActiveTileLongitude;
|
|
ActiveWeatherDebug.Provider = ActiveWeatherInputs.Provider;
|
|
ActiveWeatherDebug.ProviderTimestamp = ActiveWeatherInputs.ProviderTimestamp;
|
|
ActiveWeatherDebug.AppliedWeather = ActiveWeatherInputs.MappedWeather;
|
|
ActiveWeatherDebug.ProviderWeatherCode = ActiveWeatherInputs.ProviderWeatherCode;
|
|
ActiveWeatherDebug.TemperatureC = ActiveWeatherInputs.TemperatureC;
|
|
ActiveWeatherDebug.PrecipitationMm = ActiveWeatherInputs.PrecipitationMm;
|
|
ActiveWeatherDebug.WindSpeedKmh = ActiveWeatherInputs.WindSpeedKmh;
|
|
ActiveWeatherDebug.CloudCoverPercent = ActiveWeatherInputs.CloudCoverPercent;
|
|
ActiveWeatherDebug.RelativeHumidityPercent = ActiveWeatherInputs.RelativeHumidityPercent;
|
|
ActiveWeatherDebug.PressureMslHpa = ActiveWeatherInputs.PressureMslHpa;
|
|
ActiveWeatherDebug.VisibilityMeters = ActiveWeatherInputs.VisibilityMeters;
|
|
ActiveWeatherDebug.bHasProviderData = ActiveWeatherInputs.bHasProviderData;
|
|
|
|
SetRegionalTemperatureProfile(ActiveWeatherInputs.DailyLowTemperatureC, ActiveWeatherInputs.DailyHighTemperatureC);
|
|
SetRegionalObservedTemperature(
|
|
ActiveWeatherInputs.TemperatureC,
|
|
1.0f,
|
|
FString::Printf(TEXT("%s:%s"), *ActiveWeatherInputs.Provider, *ActiveWeatherInputs.ProviderTimestamp));
|
|
SetWeather(ActiveWeatherInputs.MappedWeather);
|
|
}
|
|
|
|
FAgrarianWeatherDebugSnapshot AAgrarianGameState::GetWeatherDebugSnapshot() const
|
|
{
|
|
return ActiveWeatherDebug;
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (!HasAuthority() || TileId == NAME_None)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ActiveSolarTileId = TileId;
|
|
ActiveTileLatitude = FMath::Clamp(Latitude, -90.0f, 90.0f);
|
|
ActiveTileLongitude = FMath::Clamp(Longitude, -180.0f, 180.0f);
|
|
ActiveTileTimeZoneId = TimeZoneId;
|
|
ActiveTileUtcOffsetHours = FMath::Clamp(UtcOffsetHours, -12.0f, 14.0f);
|
|
ActiveGrowingSeason.TileId = TileId;
|
|
UpdateSolarTimes();
|
|
UpdateAmbientTemperature();
|
|
return bHasActiveTileSolarData;
|
|
}
|
|
|
|
FAgrarianCalendarSnapshot AAgrarianGameState::GetCalendarSnapshot() const
|
|
{
|
|
FAgrarianCalendarSnapshot Snapshot;
|
|
Snapshot.DayOfYear = NormalizeCalendarDay(ActiveDayOfYear, DaysPerAgrarianYear);
|
|
Snapshot.Year = FMath::Max(1, ActiveYear);
|
|
Snapshot.AbsoluteDay = GetAbsoluteAgrarianDay();
|
|
Snapshot.DayOfSeason = GetDayOfSeason(Snapshot.DayOfYear);
|
|
Snapshot.Season = GetSeasonForDay(Snapshot.DayOfYear);
|
|
Snapshot.HourOfDay = FMath::Clamp(WorldHours, 0.0f, 24.0f);
|
|
Snapshot.bInsideGrowingSeason = IsDayInsideActiveGrowingSeason(Snapshot.DayOfYear);
|
|
Snapshot.GrowingSeasonDaysRemaining = GetDaysRemainingInActiveGrowingSeason(Snapshot.DayOfYear);
|
|
return Snapshot;
|
|
}
|
|
|
|
int32 AAgrarianGameState::GetAbsoluteAgrarianDay() const
|
|
{
|
|
const int32 SafeYear = FMath::Max(1, ActiveYear);
|
|
const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear);
|
|
return ((SafeYear - 1) * SafeDaysPerYear) + NormalizeCalendarDay(ActiveDayOfYear, SafeDaysPerYear);
|
|
}
|
|
|
|
EAgrarianSeason AAgrarianGameState::GetSeasonForDay(int32 DayOfYear) const
|
|
{
|
|
const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear);
|
|
const int32 SeasonalDay = ActiveTileLatitude < 0.0f
|
|
? NormalizeCalendarDay(DayOfYear + (SafeDaysPerYear / 2), SafeDaysPerYear)
|
|
: NormalizeCalendarDay(DayOfYear, SafeDaysPerYear);
|
|
|
|
if (SeasonalDay >= 355 || SeasonalDay < 80)
|
|
{
|
|
return EAgrarianSeason::Winter;
|
|
}
|
|
if (SeasonalDay < 172)
|
|
{
|
|
return EAgrarianSeason::Spring;
|
|
}
|
|
if (SeasonalDay < 264)
|
|
{
|
|
return EAgrarianSeason::Summer;
|
|
}
|
|
return EAgrarianSeason::Autumn;
|
|
}
|
|
|
|
int32 AAgrarianGameState::GetDayOfSeason(int32 DayOfYear) const
|
|
{
|
|
const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear);
|
|
const int32 SeasonalDay = ActiveTileLatitude < 0.0f
|
|
? NormalizeCalendarDay(DayOfYear + (SafeDaysPerYear / 2), SafeDaysPerYear)
|
|
: NormalizeCalendarDay(DayOfYear, SafeDaysPerYear);
|
|
|
|
switch (GetSeasonForDay(DayOfYear))
|
|
{
|
|
case EAgrarianSeason::Spring:
|
|
return SeasonalDay - 79;
|
|
case EAgrarianSeason::Summer:
|
|
return SeasonalDay - 171;
|
|
case EAgrarianSeason::Autumn:
|
|
return SeasonalDay - 263;
|
|
default:
|
|
return SeasonalDay >= 355 ? SeasonalDay - 354 : SeasonalDay + 12;
|
|
}
|
|
}
|
|
|
|
float AAgrarianGameState::ConvertAgrarianDaysToRealHours(float AgrarianDays) const
|
|
{
|
|
const float SafeGameHoursPerRealMinute = FMath::Max(0.001f, GameHoursPerRealMinute);
|
|
return FMath::Max(0.0f, AgrarianDays) * (24.0f / SafeGameHoursPerRealMinute) / 60.0f;
|
|
}
|
|
|
|
float AAgrarianGameState::ConvertRealHoursToAgrarianDays(float RealHours) const
|
|
{
|
|
return (FMath::Max(0.0f, RealHours) * 60.0f * FMath::Max(0.001f, GameHoursPerRealMinute)) / 24.0f;
|
|
}
|
|
|
|
float AAgrarianGameState::GetLongTaskProgress(int32 StartAbsoluteDay, float StartHourOfDay, float DurationAgrarianDays) const
|
|
{
|
|
const float SafeDuration = FMath::Max(0.001f, DurationAgrarianDays);
|
|
const float CurrentAgrarianDay = static_cast<float>(GetAbsoluteAgrarianDay() - 1) + (WorldHours / 24.0f);
|
|
const float StartAgrarianDay = static_cast<float>(FMath::Max(1, StartAbsoluteDay) - 1)
|
|
+ (FMath::Clamp(StartHourOfDay, 0.0f, 24.0f) / 24.0f);
|
|
return FMath::Clamp((CurrentAgrarianDay - StartAgrarianDay) / SafeDuration, 0.0f, 1.0f);
|
|
}
|
|
|
|
bool AAgrarianGameState::IsDayInsideActiveGrowingSeason(int32 DayOfYear) const
|
|
{
|
|
return IsDayInRange(
|
|
DayOfYear,
|
|
ActiveGrowingSeason.GrowingSeasonStartDay,
|
|
ActiveGrowingSeason.GrowingSeasonEndDay,
|
|
DaysPerAgrarianYear);
|
|
}
|
|
|
|
int32 AAgrarianGameState::GetDaysRemainingInActiveGrowingSeason(int32 PlantDayOfYear) const
|
|
{
|
|
const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear);
|
|
const int32 Day = NormalizeCalendarDay(PlantDayOfYear, SafeDaysPerYear);
|
|
const int32 Start = NormalizeCalendarDay(ActiveGrowingSeason.GrowingSeasonStartDay, SafeDaysPerYear);
|
|
const int32 End = NormalizeCalendarDay(ActiveGrowingSeason.GrowingSeasonEndDay, SafeDaysPerYear);
|
|
|
|
if (Start <= End)
|
|
{
|
|
if (Day < Start)
|
|
{
|
|
return End - Start + 1;
|
|
}
|
|
if (Day > End)
|
|
{
|
|
return 0;
|
|
}
|
|
return End - Day + 1;
|
|
}
|
|
|
|
if (Day >= Start)
|
|
{
|
|
return (SafeDaysPerYear - Day + 1) + End;
|
|
}
|
|
if (Day <= End)
|
|
{
|
|
return End - Day + 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
FAgrarianCropSeasonAssessment AAgrarianGameState::AssessCropForActiveGrowingSeason(int32 CropMaturityDays, int32 PlantDayOfYear) const
|
|
{
|
|
FAgrarianCropSeasonAssessment Assessment;
|
|
Assessment.DaysAvailable = GetDaysRemainingInActiveGrowingSeason(PlantDayOfYear);
|
|
Assessment.SafetyBufferDays = FMath::Max(0, ActiveGrowingSeason.CropSafetyBufferDays);
|
|
Assessment.DaysRequired = FMath::Max(0, CropMaturityDays) + Assessment.SafetyBufferDays;
|
|
Assessment.GrowingZoneLabel = ActiveGrowingSeason.GrowingZoneLabel;
|
|
Assessment.bCanPlantToday = false;
|
|
|
|
if (CropMaturityDays <= 0)
|
|
{
|
|
Assessment.Fit = EAgrarianCropSeasonFit::Unknown;
|
|
Assessment.Reason = FText::FromString(TEXT("Crop maturity days must be greater than zero."));
|
|
return Assessment;
|
|
}
|
|
if (!IsDayInsideActiveGrowingSeason(PlantDayOfYear))
|
|
{
|
|
Assessment.Fit = EAgrarianCropSeasonFit::OutOfSeason;
|
|
Assessment.Reason = FText::FromString(TEXT("Planting day is outside the active tile growing season."));
|
|
return Assessment;
|
|
}
|
|
if (Assessment.DaysRequired > ActiveGrowingSeason.FrostFreeDays)
|
|
{
|
|
Assessment.Fit = EAgrarianCropSeasonFit::TooLong;
|
|
Assessment.Reason = FText::FromString(TEXT("Crop maturity is too long for this tile's frost-free growing window."));
|
|
return Assessment;
|
|
}
|
|
if (Assessment.DaysRequired > Assessment.DaysAvailable)
|
|
{
|
|
Assessment.Fit = EAgrarianCropSeasonFit::Marginal;
|
|
Assessment.Reason = FText::FromString(TEXT("Crop can grow in this zone, but this planting date is too late for a reliable harvest."));
|
|
return Assessment;
|
|
}
|
|
|
|
Assessment.Fit = EAgrarianCropSeasonFit::FitsSeason;
|
|
Assessment.bCanPlantToday = true;
|
|
Assessment.Reason = FText::FromString(TEXT("Crop maturity fits the active tile growing season."));
|
|
return Assessment;
|
|
}
|
|
|
|
void AAgrarianGameState::OnRep_Weather()
|
|
{
|
|
UpdateAmbientTemperature();
|
|
}
|
|
|
|
void AAgrarianGameState::UpdateSolarTimes()
|
|
{
|
|
if (ActiveSolarTileId == NAME_None)
|
|
{
|
|
bHasActiveTileSolarData = false;
|
|
SunriseHourLocal = 6.0f;
|
|
SunsetHourLocal = 20.0f;
|
|
SolarNoonHourLocal = 13.0f;
|
|
DayLengthHours = 14.0f;
|
|
return;
|
|
}
|
|
|
|
const float ClampedLatitude = FMath::Clamp(ActiveTileLatitude, -89.8f, 89.8f);
|
|
const float ClampedLongitude = FMath::Clamp(ActiveTileLongitude, -180.0f, 180.0f);
|
|
const int32 ClampedDayOfYear = FMath::Clamp(ActiveDayOfYear, 1, 366);
|
|
// NOAA approximate sunrise/sunset model; good enough for MVP regional light timing.
|
|
const double Gamma = (2.0 * PI / 365.0) * (static_cast<double>(ClampedDayOfYear) - 1.0);
|
|
const double EquationOfTimeMinutes = 229.18 * (
|
|
0.000075
|
|
+ 0.001868 * FMath::Cos(Gamma)
|
|
- 0.032077 * FMath::Sin(Gamma)
|
|
- 0.014615 * FMath::Cos(2.0 * Gamma)
|
|
- 0.040849 * FMath::Sin(2.0 * Gamma));
|
|
const double SolarDeclinationRadians =
|
|
0.006918
|
|
- 0.399912 * FMath::Cos(Gamma)
|
|
+ 0.070257 * FMath::Sin(Gamma)
|
|
- 0.006758 * FMath::Cos(2.0 * Gamma)
|
|
+ 0.000907 * FMath::Sin(2.0 * Gamma)
|
|
- 0.002697 * FMath::Cos(3.0 * Gamma)
|
|
+ 0.00148 * FMath::Sin(3.0 * Gamma);
|
|
|
|
const double LatitudeRadians = FMath::DegreesToRadians(static_cast<double>(ClampedLatitude));
|
|
const double SolarZenithRadians = FMath::DegreesToRadians(90.833);
|
|
const double HourAngleArgument =
|
|
(FMath::Cos(SolarZenithRadians) / (FMath::Cos(LatitudeRadians) * FMath::Cos(SolarDeclinationRadians)))
|
|
- FMath::Tan(LatitudeRadians) * FMath::Tan(SolarDeclinationRadians);
|
|
|
|
if (HourAngleArgument <= -1.0 || HourAngleArgument >= 1.0)
|
|
{
|
|
bHasActiveTileSolarData = true;
|
|
SolarNoonHourLocal = FMath::Fmod(12.0f + ActiveTileUtcOffsetHours - (ClampedLongitude / 15.0f), 24.0f);
|
|
if (SolarNoonHourLocal < 0.0f)
|
|
{
|
|
SolarNoonHourLocal += 24.0f;
|
|
}
|
|
DayLengthHours = HourAngleArgument <= -1.0 ? 24.0f : 0.0f;
|
|
SunriseHourLocal = DayLengthHours >= 24.0f ? 0.0f : SolarNoonHourLocal;
|
|
SunsetHourLocal = DayLengthHours >= 24.0f ? 24.0f : SolarNoonHourLocal;
|
|
return;
|
|
}
|
|
|
|
const double HourAngleDegrees = FMath::RadiansToDegrees(FMath::Acos(HourAngleArgument));
|
|
const double SolarNoonMinutes = 720.0 - (4.0 * ClampedLongitude) - EquationOfTimeMinutes + (ActiveTileUtcOffsetHours * 60.0);
|
|
const double SunriseMinutes = SolarNoonMinutes - (HourAngleDegrees * 4.0);
|
|
const double SunsetMinutes = SolarNoonMinutes + (HourAngleDegrees * 4.0);
|
|
|
|
auto NormalizeHour = [](double Minutes)
|
|
{
|
|
double Hours = FMath::Fmod(Minutes / 60.0, 24.0);
|
|
if (Hours < 0.0)
|
|
{
|
|
Hours += 24.0;
|
|
}
|
|
return static_cast<float>(Hours);
|
|
};
|
|
|
|
bHasActiveTileSolarData = true;
|
|
SunriseHourLocal = NormalizeHour(SunriseMinutes);
|
|
SunsetHourLocal = NormalizeHour(SunsetMinutes);
|
|
SolarNoonHourLocal = NormalizeHour(SolarNoonMinutes);
|
|
DayLengthHours = static_cast<float>((SunsetMinutes - SunriseMinutes) / 60.0);
|
|
}
|
|
|
|
void AAgrarianGameState::UpdateAmbientTemperature()
|
|
{
|
|
const float ClearSkyTemperatureC = GetClearSkyTemperatureForHour(WorldHours);
|
|
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;
|
|
|
|
switch (Weather)
|
|
{
|
|
case EAgrarianWeatherType::Rain:
|
|
WeatherModifier = -2.0f;
|
|
break;
|
|
case EAgrarianWeatherType::ColdWind:
|
|
WeatherModifier = -8.0f;
|
|
break;
|
|
case EAgrarianWeatherType::Storm:
|
|
WeatherModifier = -5.0f;
|
|
break;
|
|
default:
|
|
WeatherModifier = 0.0f;
|
|
break;
|
|
}
|
|
|
|
AmbientTemperatureC = FMath::Clamp(BaseTemperatureC + WeatherModifier, -80.0f, 70.0f);
|
|
}
|