405 lines
13 KiB
C++
405 lines
13 KiB
C++
// Copyright Pacificao. All Rights Reserved.
|
|
|
|
#include "AgrarianGameState.h"
|
|
#include "Net/UnrealNetwork.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;
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
void AAgrarianGameState::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
if (HasAuthority())
|
|
{
|
|
UpdateSolarTimes();
|
|
}
|
|
}
|
|
|
|
void AAgrarianGameState::Tick(float DeltaSeconds)
|
|
{
|
|
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, 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;
|
|
UpdateAmbientTemperature();
|
|
OnRep_Weather();
|
|
}
|
|
}
|
|
|
|
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 WarmestHour = bHasActiveTileSolarData ? FMath::Fmod(SolarNoonHourLocal + 3.0f, 24.0f) : 14.0f;
|
|
const float DayWarmth = FMath::Sin(((WorldHours - WarmestHour) / 24.0f) * 2.0f * PI + (PI * 0.5f)) * 8.0f;
|
|
float WeatherModifier = 0.0f;
|
|
|
|
switch (Weather)
|
|
{
|
|
case EAgrarianWeatherType::Rain:
|
|
WeatherModifier = -3.0f;
|
|
break;
|
|
case EAgrarianWeatherType::ColdWind:
|
|
WeatherModifier = -8.0f;
|
|
break;
|
|
case EAgrarianWeatherType::Storm:
|
|
WeatherModifier = -5.0f;
|
|
break;
|
|
default:
|
|
WeatherModifier = 0.0f;
|
|
break;
|
|
}
|
|
|
|
AmbientTemperatureC = 10.0f + DayWarmth + WeatherModifier;
|
|
}
|