Complete early roadmap foundation and calendar helpers

This commit is contained in:
2026-05-15 21:41:37 -07:00
parent 6cd6729b7b
commit 8ee1f83b16
80 changed files with 3354 additions and 157 deletions
+117 -2
View File
@@ -5,6 +5,7 @@
#include "AgrarianInventoryComponent.h"
#include "AgrarianSurvivalComponent.h"
#include "Engine/Canvas.h"
#include "GameFramework/CharacterMovementComponent.h"
void AAgrarianDebugHUD::DrawHUD()
{
@@ -22,6 +23,7 @@ void AAgrarianDebugHUD::DrawHUD()
}
DrawInteractionPrompt(AgrarianCharacter);
DrawCriticalStats(AgrarianCharacter->GetSurvivalComponent());
if (bShowDebugHUD)
{
@@ -29,6 +31,7 @@ void AAgrarianDebugHUD::DrawHUD()
constexpr float X = 32.0f;
DrawLine(TEXT("AGRARIAN DEV HUD"), X, Y, FColor(160, 220, 140));
DrawPlayerStatus(AgrarianCharacter, X, Y);
DrawSurvival(AgrarianCharacter->GetSurvivalComponent(), X, Y);
DrawInventory(AgrarianCharacter->GetInventoryComponent(), X, Y);
}
@@ -51,6 +54,108 @@ void AAgrarianDebugHUD::DrawInteractionPrompt(const AAgrarianGameCharacter* Agra
DrawText(Prompt, FColor(245, 245, 225), X, Y, nullptr, PromptTextScale, true);
}
void AAgrarianDebugHUD::DrawCriticalStats(const UAgrarianSurvivalComponent* SurvivalComponent)
{
if (!bShowCriticalStatsHUD || !SurvivalComponent || !Canvas)
{
return;
}
const FAgrarianSurvivalSnapshot& Survival = SurvivalComponent->Survival;
float Y = FMath::Max(32.0f, Canvas->ClipY - (178.0f * CriticalStatsTextScale));
constexpr float X = 32.0f;
const FColor CriticalColor(230, 90, 70);
const FColor WarningColor(245, 190, 80);
const FColor StableColor(225, 235, 220);
const FColor HeaderColor(160, 220, 140);
auto StatusColor = [CriticalColor, WarningColor, StableColor](float Value, bool bHigherIsWorse = false)
{
if (bHigherIsWorse)
{
if (Value >= 75.0f)
{
return CriticalColor;
}
if (Value >= 40.0f)
{
return WarningColor;
}
return StableColor;
}
if (Value <= 20.0f)
{
return CriticalColor;
}
if (Value <= 45.0f)
{
return WarningColor;
}
return StableColor;
};
DrawScaledLine(TEXT("SURVIVAL"), X, Y, CriticalStatsTextScale, HeaderColor);
DrawScaledLine(FString::Printf(TEXT("Health %3.0f"), Survival.Health), X, Y, CriticalStatsTextScale, StatusColor(Survival.Health));
DrawScaledLine(FString::Printf(TEXT("Stamina %3.0f"), Survival.Stamina), X, Y, CriticalStatsTextScale, StatusColor(Survival.Stamina));
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("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));
}
void AAgrarianDebugHUD::DrawPlayerStatus(const AAgrarianGameCharacter* AgrarianCharacter, float X, float& Y)
{
if (!AgrarianCharacter)
{
DrawLine(TEXT("Player: unavailable"), X, Y, FColor::Red);
return;
}
const UCharacterMovementComponent* MovementComponent = AgrarianCharacter->GetCharacterMovement();
const FVector Location = AgrarianCharacter->GetActorLocation();
const float HorizontalSpeed = AgrarianCharacter->GetVelocity().Size2D();
const FString MovementMode = MovementComponent
? UEnum::GetValueAsString(MovementComponent->MovementMode)
: TEXT("Unavailable");
DrawLine(FString::Printf(
TEXT("Role: %s | Local: %s"),
AgrarianCharacter->HasAuthority() ? TEXT("Authority") : TEXT("Simulated/Autonomous"),
AgrarianCharacter->IsLocallyControlled() ? TEXT("yes") : TEXT("no")),
X,
Y,
FColor(160, 220, 140));
DrawLine(FString::Printf(TEXT("Location: X %.0f Y %.0f Z %.0f"), Location.X, Location.Y, Location.Z), X, Y);
DrawLine(FString::Printf(TEXT("Speed: %.0f uu/s | Mode: %s"), HorizontalSpeed, *MovementMode), X, Y);
DrawLine(FString::Printf(
TEXT("Stance: %s | Sprint: %s | Camera: %s"),
AgrarianCharacter->IsProne() ? TEXT("Prone") : (AgrarianCharacter->bIsCrouched ? TEXT("Crouched") : TEXT("Standing")),
AgrarianCharacter->IsSprinting() ? TEXT("yes") : TEXT("no"),
AgrarianCharacter->IsFirstPersonCamera() ? TEXT("First") : TEXT("Third")),
X,
Y);
DrawLine(FString::Printf(
TEXT("Move Mult: %.2f | Carry: %.1f"),
AgrarianCharacter->GetCurrentMovementSpeedMultiplier(),
AgrarianCharacter->GetCurrentCarryWeight()),
X,
Y);
DrawLine(FString::Printf(
TEXT("Age %.1f | Cond %.2f | Str %.2f | End %.2f | Terrain %.2f"),
AgrarianCharacter->GetAgeYears(),
AgrarianCharacter->GetPhysicalConditionMultiplier(),
AgrarianCharacter->GetStrengthMultiplier(),
AgrarianCharacter->GetEnduranceMultiplier(),
AgrarianCharacter->GetTerrainMovementMultiplier()),
X,
Y);
Y += 10.0f * TextScale;
}
void AAgrarianDebugHUD::DrawSurvival(const UAgrarianSurvivalComponent* SurvivalComponent, float X, float& Y)
{
if (!SurvivalComponent)
@@ -62,10 +167,15 @@ void AAgrarianDebugHUD::DrawSurvival(const UAgrarianSurvivalComponent* SurvivalC
const FAgrarianSurvivalSnapshot& Survival = SurvivalComponent->Survival;
DrawLine(FString::Printf(TEXT("Health: %.0f"), Survival.Health), X, Y);
DrawLine(FString::Printf(TEXT("Stamina: %.0f"), Survival.Stamina), X, Y);
DrawLine(FString::Printf(TEXT("Exhaust: %.0f"), Survival.Exhaustion), 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("Temp: %.1f C"), Survival.BodyTemperature), 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;
DrawLine(FString::Printf(TEXT("Care N/S/T: %.2f %.2f %.2f"), Care.NutritionQuality, Care.SleepQuality, Care.TreatmentQuality), X, Y, FColor::Silver);
DrawLine(FString::Printf(TEXT("Burden I/I/S/W: %.2f %.2f %.2f %.2f"), Care.IllnessBurden, Care.InjuryBurden, Care.StressBurden, Care.WorkloadBurden), X, Y, FColor::Silver);
Y += 10.0f * TextScale;
}
@@ -94,6 +204,11 @@ void AAgrarianDebugHUD::DrawInventory(const UAgrarianInventoryComponent* Invento
void AAgrarianDebugHUD::DrawLine(const FString& Text, float X, float& Y, const FColor& Color)
{
DrawText(Text, Color, X, Y, nullptr, TextScale, false);
Y += 18.0f * TextScale;
DrawScaledLine(Text, X, Y, TextScale, Color);
}
void AAgrarianDebugHUD::DrawScaledLine(const FString& Text, float X, float& Y, float Scale, const FColor& Color)
{
DrawText(Text, Color, X, Y, nullptr, Scale, false);
Y += 18.0f * Scale;
}
+9
View File
@@ -20,9 +20,15 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD")
bool bShowDebugHUD = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD")
bool bShowCriticalStatsHUD = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD", meta = (ClampMin = "0.25"))
float TextScale = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD", meta = (ClampMin = "0.25"))
float CriticalStatsTextScale = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|HUD")
bool bShowInteractionPrompt = true;
@@ -31,7 +37,10 @@ public:
protected:
void DrawInteractionPrompt(const class AAgrarianGameCharacter* AgrarianCharacter);
void DrawCriticalStats(const UAgrarianSurvivalComponent* SurvivalComponent);
void DrawPlayerStatus(const class AAgrarianGameCharacter* AgrarianCharacter, float X, float& Y);
void DrawSurvival(const UAgrarianSurvivalComponent* SurvivalComponent, float X, float& Y);
void DrawInventory(const UAgrarianInventoryComponent* InventoryComponent, float X, float& Y);
void DrawLine(const FString& Text, float X, float& Y, const FColor& Color = FColor::White);
void DrawScaledLine(const FString& Text, float X, float& Y, float Scale, const FColor& Color = FColor::White);
};
+37 -2
View File
@@ -21,6 +21,10 @@
AAgrarianGameCharacter::AAgrarianGameCharacter()
{
PrimaryActorTick.bCanEverTick = true;
bReplicates = true;
SetReplicateMovement(true);
SetNetUpdateFrequency(30.0f);
SetMinNetUpdateFrequency(10.0f);
// Set size for collision capsule
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
@@ -286,6 +290,7 @@ bool AAgrarianGameCharacter::CanSprint() const
&& SurvivalComponent->IsAlive()
&& !bIsProne
&& !bIsCrouched
&& SurvivalComponent->Survival.Exhaustion < 85.0f
&& SurvivalComponent->Survival.Stamina > MinSprintStamina;
}
@@ -329,7 +334,19 @@ void AAgrarianGameCharacter::SetProne(bool bNewProne)
void AAgrarianGameCharacter::SetTerrainMovementMultiplier(float NewTerrainMovementMultiplier)
{
TerrainMovementMultiplier = FMath::Clamp(NewTerrainMovementMultiplier, 0.25f, 1.25f);
const float ClampedTerrainMovementMultiplier = FMath::Clamp(NewTerrainMovementMultiplier, 0.25f, 1.25f);
if (!HasAuthority())
{
ServerSetTerrainMovementMultiplier(ClampedTerrainMovementMultiplier);
return;
}
if (FMath::IsNearlyEqual(TerrainMovementMultiplier, ClampedTerrainMovementMultiplier))
{
return;
}
TerrainMovementMultiplier = ClampedTerrainMovementMultiplier;
ApplyMovementSpeed();
}
@@ -403,8 +420,16 @@ float AAgrarianGameCharacter::CalculateSurvivalMovementMultiplier() const
FVector2D(0.0f, 100.0f),
FVector2D(1.0f, 0.5f),
Survival.InjurySeverity);
const float SicknessMultiplier = FMath::GetMappedRangeValueClamped(
FVector2D(0.0f, 100.0f),
FVector2D(1.0f, 0.7f),
Survival.SicknessSeverity);
const float ExhaustionMultiplier = FMath::GetMappedRangeValueClamped(
FVector2D(0.0f, 100.0f),
FVector2D(1.0f, 0.55f),
Survival.Exhaustion);
return HungerMultiplier * ThirstMultiplier * InjuryMultiplier;
return HungerMultiplier * ThirstMultiplier * InjuryMultiplier * SicknessMultiplier * ExhaustionMultiplier;
}
float AAgrarianGameCharacter::CalculateCarryWeightMovementMultiplier() const
@@ -460,6 +485,11 @@ void AAgrarianGameCharacter::OnRep_ProneState()
ApplyMovementSpeed();
}
void AAgrarianGameCharacter::OnRep_MovementModifierState()
{
ApplyMovementSpeed();
}
void AAgrarianGameCharacter::DoMove(float Right, float Forward)
{
if (GetController() != nullptr)
@@ -585,6 +615,11 @@ void AAgrarianGameCharacter::ServerSetProne_Implementation(bool bNewProne)
SetProne(bNewProne);
}
void AAgrarianGameCharacter::ServerSetTerrainMovementMultiplier_Implementation(float NewTerrainMovementMultiplier)
{
SetTerrainMovementMultiplier(NewTerrainMovementMultiplier);
}
void AAgrarianGameCharacter::ServerInteract_Implementation(AActor* TargetActor)
{
if (!TargetActor || !TargetActor->GetClass()->ImplementsInterface(UAgrarianInteractable::StaticClass()))
+27 -5
View File
@@ -126,19 +126,19 @@ protected:
float ProneSpeedMultiplier = 0.25f;
/** Age hook used by movement until full lifecycle/aging systems own it. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0"))
UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_MovementModifierState, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0"))
float AgeYears = 25.0f;
/** General physical condition scalar reserved for care, illness, sleep, and long-term state. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "1.25"))
UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_MovementModifierState, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "1.25"))
float PhysicalConditionMultiplier = 1.0f;
/** Strength scalar that mainly offsets carried-weight penalties. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "2.0"))
UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_MovementModifierState, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "2.0"))
float StrengthMultiplier = 1.0f;
/** Endurance scalar that improves stamina efficiency and movement resilience. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "2.0"))
UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_MovementModifierState, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "2.0"))
float EnduranceMultiplier = 1.0f;
/** Comfort carry capacity before speed penalties, in item-weight units. */
@@ -150,7 +150,7 @@ protected:
float HeavyCarryWeight = 60.0f;
/** Terrain hook for later surface/volume systems. Values below one slow the character. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "1.25"))
UPROPERTY(EditAnywhere, BlueprintReadWrite, ReplicatedUsing = OnRep_MovementModifierState, Category="Agrarian|Movement|Modifiers", meta = (ClampMin = "0.25", ClampMax = "1.25"))
float TerrainMovementMultiplier = 1.0f;
/** Third-person spring arm distance used when returning from first person. */
@@ -238,6 +238,9 @@ protected:
UFUNCTION()
void OnRep_ProneState();
UFUNCTION()
void OnRep_MovementModifierState();
public:
/** Handles move inputs from either controls or UI interfaces */
@@ -292,6 +295,21 @@ public:
UFUNCTION(BlueprintPure, Category="Agrarian|Movement")
float GetCurrentMovementSpeedMultiplier() const;
UFUNCTION(BlueprintPure, Category="Agrarian|Movement|Modifiers")
float GetAgeYears() const { return AgeYears; }
UFUNCTION(BlueprintPure, Category="Agrarian|Movement|Modifiers")
float GetPhysicalConditionMultiplier() const { return PhysicalConditionMultiplier; }
UFUNCTION(BlueprintPure, Category="Agrarian|Movement|Modifiers")
float GetStrengthMultiplier() const { return StrengthMultiplier; }
UFUNCTION(BlueprintPure, Category="Agrarian|Movement|Modifiers")
float GetEnduranceMultiplier() const { return EnduranceMultiplier; }
UFUNCTION(BlueprintPure, Category="Agrarian|Movement|Modifiers")
float GetTerrainMovementMultiplier() const { return TerrainMovementMultiplier; }
UFUNCTION(BlueprintCallable, Category="Agrarian|Movement")
void SetTerrainMovementMultiplier(float NewTerrainMovementMultiplier);
@@ -307,6 +325,10 @@ public:
UFUNCTION(Server, Reliable)
void ServerSetProne(bool bNewProne);
/** Server-authoritative terrain movement modifier update. */
UFUNCTION(Server, Reliable)
void ServerSetTerrainMovementMultiplier(float NewTerrainMovementMultiplier);
/** Refreshes local interactable focus and prompt text. */
void UpdateInteractionPrompt();
@@ -104,13 +104,15 @@ void AAgrarianGamePlayerController::AgrarianSurvival()
const FAgrarianSurvivalSnapshot& Survival = SurvivalComponent->Survival;
ClientMessage(FString::Printf(
TEXT("Health %.1f | Stamina %.1f | Hunger %.1f | Thirst %.1f | Temp %.1fC | Injury %.1f"),
TEXT("Health %.1f | Stamina %.1f | Exhaustion %.1f | Hunger %.1f | Thirst %.1f | Temp %.1fC | Injury %.1f | Sickness %.1f"),
Survival.Health,
Survival.Stamina,
Survival.Exhaustion,
Survival.Hunger,
Survival.Thirst,
Survival.BodyTemperature,
Survival.InjurySeverity));
Survival.InjurySeverity,
Survival.SicknessSeverity));
}
void AAgrarianGamePlayerController::AgrarianHeal()
@@ -167,8 +169,9 @@ void AAgrarianGamePlayerController::ServerAgrarianLoadWorld_Implementation()
Persistence->RegisterWorldActorClass(TEXT("primitive_shelter"), AAgrarianShelterActor::StaticClass());
const UAgrarianSaveGame* SaveGame = Persistence->LoadOrCreateSave();
const int32 RestoredPlayerCount = Persistence->RestorePlayers(SaveGame);
const int32 RestoredCount = Persistence->RestoreWorldActors(SaveGame);
ClientMessage(FString::Printf(TEXT("Agrarian world loaded. Restored actors: %d."), RestoredCount));
ClientMessage(FString::Printf(TEXT("Agrarian world loaded. Restored players: %d. Restored actors: %d."), RestoredPlayerCount, RestoredCount));
}
void AAgrarianGamePlayerController::ServerAgrarianHeal_Implementation()
@@ -184,6 +187,8 @@ void AAgrarianGamePlayerController::ServerAgrarianHeal_Implementation()
SurvivalComponent->RestoreHealth(100.0f);
SurvivalComponent->AddFood(100.0f);
SurvivalComponent->AddWater(100.0f);
SurvivalComponent->ReduceExhaustion(100.0f);
SurvivalComponent->ReduceSickness(100.0f);
SurvivalComponent->AddWarmth(37.0f - SurvivalComponent->Survival.BodyTemperature);
ClientMessage(TEXT("Agrarian survival restored."));
}
+325 -1
View File
@@ -3,10 +3,52 @@
#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)
@@ -22,6 +64,13 @@ void AAgrarianGameState::Tick(float DeltaSeconds)
while (WorldHours >= 24.0f)
{
WorldHours -= 24.0f;
const int32 PreviousDayOfYear = ActiveDayOfYear;
ActiveDayOfYear = (ActiveDayOfYear % FMath::Max(1, DaysPerAgrarianYear)) + 1;
if (ActiveDayOfYear < PreviousDayOfYear)
{
++ActiveYear;
}
UpdateSolarTimes();
}
UpdateAmbientTemperature();
@@ -33,10 +82,29 @@ void AAgrarianGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& O
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;
}
@@ -50,14 +118,270 @@ void AAgrarianGameState::SetWeather(EAgrarianWeatherType NewWeather)
}
}
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 DayWarmth = FMath::Sin((WorldHours / 24.0f) * 2.0f * PI - (PI * 0.5f)) * 8.0f;
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)
+77
View File
@@ -15,6 +15,7 @@ class AAgrarianGameState : public AGameStateBase
public:
AAgrarianGameState();
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
@@ -24,21 +25,97 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|World", meta = (ClampMin = "0.1"))
float GameHoursPerRealMinute = 0.1f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Calendar", meta = (ClampMin = "1", ClampMax = "366"))
int32 DaysPerAgrarianYear = 366;
UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_Weather, Category = "Agrarian|World")
EAgrarianWeatherType Weather = EAgrarianWeatherType::Clear;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World")
float AmbientTemperatureC = 12.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
FName ActiveSolarTileId = TEXT("gz_us_ca_pacifica_utm10n_e544_n4160");
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
float ActiveTileLatitude = 37.5925f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
float ActiveTileLongitude = -122.4995f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
FString ActiveTileTimeZoneId = TEXT("America/Los_Angeles");
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
float ActiveTileUtcOffsetHours = -7.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar", meta = (ClampMin = "1", ClampMax = "366"))
int32 ActiveDayOfYear = 172;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Calendar", meta = (ClampMin = "1"))
int32 ActiveYear = 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Growing Season")
FAgrarianGrowingSeasonProfile ActiveGrowingSeason;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
bool bHasActiveTileSolarData = false;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
float SunriseHourLocal = 6.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
float SunsetHourLocal = 20.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
float SolarNoonHourLocal = 13.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated, Category = "Agrarian|World|Tile Solar")
float DayLengthHours = 14.0f;
UFUNCTION(BlueprintCallable, Category = "Agrarian|World")
bool IsNight() const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|World")
void SetWeather(EAgrarianWeatherType NewWeather);
UFUNCTION(BlueprintCallable, Category = "Agrarian|World|Tile Solar")
bool ConfigureActiveSolarTile(FName TileId, float Latitude, float Longitude, const FString& TimeZoneId, float UtcOffsetHours);
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar")
FAgrarianCalendarSnapshot GetCalendarSnapshot() const;
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar")
int32 GetAbsoluteAgrarianDay() const;
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar")
EAgrarianSeason GetSeasonForDay(int32 DayOfYear) const;
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar")
int32 GetDayOfSeason(int32 DayOfYear) const;
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar")
float ConvertAgrarianDaysToRealHours(float AgrarianDays) const;
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar")
float ConvertRealHoursToAgrarianDays(float RealHours) const;
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Calendar")
float GetLongTaskProgress(int32 StartAbsoluteDay, float StartHourOfDay, float DurationAgrarianDays) const;
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Growing Season")
bool IsDayInsideActiveGrowingSeason(int32 DayOfYear) const;
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Growing Season")
int32 GetDaysRemainingInActiveGrowingSeason(int32 PlantDayOfYear) const;
UFUNCTION(BlueprintPure, Category = "Agrarian|World|Growing Season")
FAgrarianCropSeasonAssessment AssessCropForActiveGrowingSeason(int32 CropMaturityDays, int32 PlantDayOfYear) const;
protected:
UFUNCTION()
void OnRep_Weather();
void UpdateSolarTimes();
void UpdateAmbientTemperature();
};
@@ -1,10 +1,14 @@
// Copyright Pacificao. All Rights Reserved.
#include "AgrarianPersistenceSubsystem.h"
#include "AgrarianGameCharacter.h"
#include "AgrarianInventoryComponent.h"
#include "AgrarianPersistentActorComponent.h"
#include "AgrarianSaveGame.h"
#include "AgrarianSurvivalComponent.h"
#include "EngineUtils.h"
#include "Engine/World.h"
#include "GameFramework/PlayerState.h"
#include "Kismet/GameplayStatics.h"
UAgrarianSaveGame* UAgrarianPersistenceSubsystem::CreateEmptySave() const
@@ -116,6 +120,87 @@ int32 UAgrarianPersistenceSubsystem::RestoreWorldActors(const UAgrarianSaveGame*
return RestoredCount;
}
int32 UAgrarianPersistenceSubsystem::CapturePlayers(UAgrarianSaveGame* SaveGame) const
{
if (!SaveGame)
{
return 0;
}
TArray<AAgrarianGameCharacter*> Players;
FindAgrarianPlayers(Players);
SaveGame->Players.Reset();
for (const AAgrarianGameCharacter* Character : Players)
{
const UAgrarianSurvivalComponent* SurvivalComponent = Character ? Character->GetSurvivalComponent() : nullptr;
if (!Character || !SurvivalComponent)
{
continue;
}
FAgrarianSavedPlayer SavedPlayer;
SavedPlayer.PlayerId = GetPlayerPersistenceId(Character);
SavedPlayer.Transform = Character->GetActorTransform();
SavedPlayer.Survival = SurvivalComponent->Survival;
SavedPlayer.CareHistory = SurvivalComponent->CareHistory;
if (const UAgrarianInventoryComponent* InventoryComponent = Character->GetInventoryComponent())
{
SavedPlayer.Inventory = InventoryComponent->Items;
}
SaveGame->Players.Add(SavedPlayer);
}
return SaveGame->Players.Num();
}
int32 UAgrarianPersistenceSubsystem::RestorePlayers(const UAgrarianSaveGame* SaveGame) const
{
if (!SaveGame)
{
return 0;
}
TArray<AAgrarianGameCharacter*> Players;
FindAgrarianPlayers(Players);
int32 RestoredCount = 0;
for (AAgrarianGameCharacter* Character : Players)
{
UAgrarianSurvivalComponent* SurvivalComponent = Character ? Character->GetSurvivalComponent() : nullptr;
if (!Character || !SurvivalComponent)
{
continue;
}
const FString PlayerId = GetPlayerPersistenceId(Character);
const FAgrarianSavedPlayer* SavedPlayer = SaveGame->Players.FindByPredicate(
[&PlayerId](const FAgrarianSavedPlayer& Candidate)
{
return Candidate.PlayerId == PlayerId;
});
if (!SavedPlayer)
{
continue;
}
Character->SetActorTransform(SavedPlayer->Transform, false, nullptr, ETeleportType::TeleportPhysics);
SurvivalComponent->ApplySavedState(SavedPlayer->Survival, SavedPlayer->CareHistory);
if (UAgrarianInventoryComponent* InventoryComponent = Character->GetInventoryComponent())
{
InventoryComponent->Items = SavedPlayer->Inventory;
}
RestoredCount++;
}
return RestoredCount;
}
bool UAgrarianPersistenceSubsystem::SaveCurrentWorld() const
{
UAgrarianSaveGame* SaveGame = LoadOrCreateSave();
@@ -124,6 +209,7 @@ bool UAgrarianPersistenceSubsystem::SaveCurrentWorld() const
return false;
}
CapturePlayers(SaveGame);
CaptureWorldActors(SaveGame);
return WriteSave(SaveGame);
}
@@ -152,3 +238,42 @@ void UAgrarianPersistenceSubsystem::FindPersistentComponents(TArray<UAgrarianPer
}
}
}
void UAgrarianPersistenceSubsystem::FindAgrarianPlayers(TArray<AAgrarianGameCharacter*>& OutPlayers) const
{
OutPlayers.Reset();
UWorld* World = GetWorld();
if (!World)
{
return;
}
for (TActorIterator<AAgrarianGameCharacter> ActorIt(World); ActorIt; ++ActorIt)
{
AAgrarianGameCharacter* Character = *ActorIt;
if (Character && !Character->IsPendingKillPending())
{
OutPlayers.Add(Character);
}
}
}
FString UAgrarianPersistenceSubsystem::GetPlayerPersistenceId(const AAgrarianGameCharacter* Character) const
{
if (!Character)
{
return TEXT("UnknownPlayer");
}
if (const APlayerState* PlayerState = Character->GetPlayerState())
{
const FString PlayerName = PlayerState->GetPlayerName();
if (!PlayerName.IsEmpty())
{
return PlayerName;
}
}
return Character->GetName();
}
@@ -8,6 +8,7 @@
class UAgrarianSaveGame;
class UAgrarianPersistentActorComponent;
class AAgrarianGameCharacter;
UCLASS()
class UAgrarianPersistenceSubsystem : public UGameInstanceSubsystem
@@ -45,9 +46,17 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
int32 RestoreWorldActors(const UAgrarianSaveGame* SaveGame, bool bClearExistingActors = true) const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
int32 CapturePlayers(UAgrarianSaveGame* SaveGame) const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
int32 RestorePlayers(const UAgrarianSaveGame* SaveGame) const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
bool SaveCurrentWorld() const;
protected:
void FindPersistentComponents(TArray<UAgrarianPersistentActorComponent*>& OutComponents) const;
void FindAgrarianPlayers(TArray<AAgrarianGameCharacter*>& OutPlayers) const;
FString GetPlayerPersistenceId(const AAgrarianGameCharacter* Character) const;
};
+3
View File
@@ -21,6 +21,9 @@ struct FAgrarianSavedPlayer
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save")
FAgrarianSurvivalSnapshot Survival;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save")
FAgrarianCareHistorySnapshot CareHistory;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Save")
TArray<FAgrarianItemStack> Inventory;
};
@@ -15,6 +15,7 @@ void UAgrarianSurvivalComponent::BeginPlay()
{
Super::BeginPlay();
ClampSurvival();
ClampCareHistory();
BroadcastSurvivalChanged();
}
@@ -32,6 +33,26 @@ void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickT
Survival.Thirst -= ThirstDecayPerMinute * Minutes;
Survival.Stamina += StaminaRecoveryPerSecond * DeltaTime;
if (Survival.Stamina <= LowStaminaExhaustionThreshold)
{
Survival.Exhaustion += ExhaustionGainPerLowStaminaSecond * DeltaTime;
}
else if (Survival.Hunger > 10.0f && Survival.Thirst > 10.0f && Survival.BodyTemperature >= 35.0f)
{
Survival.Exhaustion -= ExhaustionRecoveryPerSecond * DeltaTime;
}
if (Survival.SicknessSeverity > 0.0f)
{
Survival.Exhaustion += (Survival.SicknessSeverity / 100.0f) * 0.08f * DeltaTime;
CareHistory.IllnessBurden += (Survival.SicknessSeverity / 100.0f) * 0.001f * DeltaTime;
if (Survival.Hunger > 20.0f && Survival.Thirst > 20.0f && Survival.BodyTemperature >= 35.0f)
{
Survival.SicknessSeverity -= SicknessRecoveryPerSecond * DeltaTime * FMath::Max(0.25f, CareHistory.TreatmentQuality);
}
}
if (const UWorld* World = GetWorld())
{
if (const AAgrarianGameState* AgrarianGameState = World->GetGameState<AAgrarianGameState>())
@@ -56,7 +77,13 @@ void UAgrarianSurvivalComponent::TickComponent(float DeltaTime, ELevelTick TickT
Survival.Health -= ColdDamagePerMinute * Minutes;
}
if (Survival.SicknessSeverity >= 60.0f)
{
Survival.Health -= SicknessDamagePerMinute * (Survival.SicknessSeverity / 100.0f) * Minutes;
}
ClampSurvival();
ClampCareHistory();
BroadcastSurvivalChanged();
}
@@ -64,6 +91,7 @@ void UAgrarianSurvivalComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProp
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UAgrarianSurvivalComponent, Survival);
DOREPLIFETIME(UAgrarianSurvivalComponent, CareHistory);
}
bool UAgrarianSurvivalComponent::IsAlive() const
@@ -126,8 +154,45 @@ void UAgrarianSurvivalComponent::AddInjury(float Severity)
if (GetOwner() && GetOwner()->HasAuthority())
{
Survival.InjurySeverity += FMath::Max(0.0f, Severity);
CareHistory.InjuryBurden += FMath::Max(0.0f, Severity) / 100.0f;
Survival.Health -= Severity * 5.0f;
ClampSurvival();
ClampCareHistory();
BroadcastSurvivalChanged();
}
}
void UAgrarianSurvivalComponent::AddSickness(float Severity)
{
if (GetOwner() && GetOwner()->HasAuthority())
{
const float PositiveSeverity = FMath::Max(0.0f, Severity);
Survival.SicknessSeverity += PositiveSeverity;
CareHistory.IllnessBurden += PositiveSeverity / 100.0f;
ClampSurvival();
ClampCareHistory();
BroadcastSurvivalChanged();
}
}
void UAgrarianSurvivalComponent::ReduceSickness(float Amount)
{
if (GetOwner() && GetOwner()->HasAuthority())
{
Survival.SicknessSeverity -= FMath::Max(0.0f, Amount);
ClampSurvival();
BroadcastSurvivalChanged();
}
}
void UAgrarianSurvivalComponent::ApplySavedState(const FAgrarianSurvivalSnapshot& SavedSurvival, const FAgrarianCareHistorySnapshot& SavedCareHistory)
{
if (GetOwner() && GetOwner()->HasAuthority())
{
Survival = SavedSurvival;
CareHistory = SavedCareHistory;
ClampSurvival();
ClampCareHistory();
BroadcastSurvivalChanged();
}
}
@@ -136,7 +201,29 @@ void UAgrarianSurvivalComponent::SpendStamina(float Amount)
{
if (GetOwner() && GetOwner()->HasAuthority())
{
Survival.Stamina -= FMath::Max(0.0f, Amount);
const float PositiveAmount = FMath::Max(0.0f, Amount);
Survival.Stamina -= PositiveAmount;
Survival.Exhaustion += PositiveAmount * 0.05f;
ClampSurvival();
BroadcastSurvivalChanged();
}
}
void UAgrarianSurvivalComponent::AddExhaustion(float Amount)
{
if (GetOwner() && GetOwner()->HasAuthority())
{
Survival.Exhaustion += FMath::Max(0.0f, Amount);
ClampSurvival();
BroadcastSurvivalChanged();
}
}
void UAgrarianSurvivalComponent::ReduceExhaustion(float Amount)
{
if (GetOwner() && GetOwner()->HasAuthority())
{
Survival.Exhaustion -= FMath::Max(0.0f, Amount);
ClampSurvival();
BroadcastSurvivalChanged();
}
@@ -147,14 +234,34 @@ void UAgrarianSurvivalComponent::OnRep_Survival()
BroadcastSurvivalChanged();
}
void UAgrarianSurvivalComponent::OnRep_CareHistory()
{
ClampCareHistory();
BroadcastSurvivalChanged();
}
void UAgrarianSurvivalComponent::ClampSurvival()
{
Survival.Health = FMath::Clamp(Survival.Health, 0.0f, 100.0f);
Survival.Stamina = FMath::Clamp(Survival.Stamina, 0.0f, 100.0f);
Survival.Exhaustion = FMath::Clamp(Survival.Exhaustion, 0.0f, 100.0f);
Survival.Hunger = FMath::Clamp(Survival.Hunger, 0.0f, 100.0f);
Survival.Thirst = FMath::Clamp(Survival.Thirst, 0.0f, 100.0f);
Survival.BodyTemperature = FMath::Clamp(Survival.BodyTemperature, 30.0f, 42.0f);
Survival.InjurySeverity = FMath::Clamp(Survival.InjurySeverity, 0.0f, 100.0f);
Survival.SicknessSeverity = FMath::Clamp(Survival.SicknessSeverity, 0.0f, 100.0f);
}
void UAgrarianSurvivalComponent::ClampCareHistory()
{
CareHistory.NutritionQuality = FMath::Clamp(CareHistory.NutritionQuality, 0.0f, 1.0f);
CareHistory.IllnessBurden = FMath::Clamp(CareHistory.IllnessBurden, 0.0f, 1.0f);
CareHistory.InjuryBurden = FMath::Clamp(CareHistory.InjuryBurden, 0.0f, 1.0f);
CareHistory.SleepQuality = FMath::Clamp(CareHistory.SleepQuality, 0.0f, 1.0f);
CareHistory.ShelterQuality = FMath::Clamp(CareHistory.ShelterQuality, 0.0f, 1.0f);
CareHistory.StressBurden = FMath::Clamp(CareHistory.StressBurden, 0.0f, 1.0f);
CareHistory.WorkloadBurden = FMath::Clamp(CareHistory.WorkloadBurden, 0.0f, 1.0f);
CareHistory.TreatmentQuality = FMath::Clamp(CareHistory.TreatmentQuality, 0.0f, 1.0f);
}
void UAgrarianSurvivalComponent::BroadcastSurvivalChanged()
@@ -27,6 +27,9 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_Survival, Category = "Agrarian|Survival")
FAgrarianSurvivalSnapshot Survival;
UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_CareHistory, Category = "Agrarian|Survival")
FAgrarianCareHistorySnapshot CareHistory;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0"))
float HungerDecayPerMinute = 0.55f;
@@ -36,6 +39,15 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0"))
float StaminaRecoveryPerSecond = 14.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0"))
float ExhaustionGainPerLowStaminaSecond = 0.35f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0"))
float ExhaustionRecoveryPerSecond = 0.08f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0", ClampMax = "100"))
float LowStaminaExhaustionThreshold = 20.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0"))
float StarvationDamagePerMinute = 3.0f;
@@ -45,6 +57,12 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0"))
float ColdDamagePerMinute = 4.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0"))
float SicknessDamagePerMinute = 1.5f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival|Rates", meta = (ClampMin = "0"))
float SicknessRecoveryPerSecond = 0.02f;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
bool IsAlive() const;
@@ -66,13 +84,32 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
void AddInjury(float Severity);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
void AddSickness(float Severity);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
void ReduceSickness(float Amount);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
void ApplySavedState(const FAgrarianSurvivalSnapshot& SavedSurvival, const FAgrarianCareHistorySnapshot& SavedCareHistory);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
void SpendStamina(float Amount);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
void AddExhaustion(float Amount);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Survival")
void ReduceExhaustion(float Amount);
protected:
UFUNCTION()
void OnRep_Survival();
UFUNCTION()
void OnRep_CareHistory();
void ClampSurvival();
void ClampCareHistory();
void BroadcastSurvivalChanged();
};
+142
View File
@@ -14,6 +14,112 @@ enum class EAgrarianWeatherType : uint8
Storm UMETA(DisplayName = "Storm")
};
UENUM(BlueprintType)
enum class EAgrarianSeason : uint8
{
Winter UMETA(DisplayName = "Winter"),
Spring UMETA(DisplayName = "Spring"),
Summer UMETA(DisplayName = "Summer"),
Autumn UMETA(DisplayName = "Autumn")
};
UENUM(BlueprintType)
enum class EAgrarianCropSeasonFit : uint8
{
Unknown UMETA(DisplayName = "Unknown"),
FitsSeason UMETA(DisplayName = "Fits Season"),
Marginal UMETA(DisplayName = "Marginal"),
TooLong UMETA(DisplayName = "Too Long"),
OutOfSeason UMETA(DisplayName = "Out Of Season")
};
USTRUCT(BlueprintType)
struct FAgrarianCalendarSnapshot
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar")
int32 DayOfYear = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar")
int32 Year = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar")
int32 AbsoluteDay = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar")
int32 DayOfSeason = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar")
EAgrarianSeason Season = EAgrarianSeason::Winter;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar")
float HourOfDay = 8.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar")
bool bInsideGrowingSeason = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Calendar")
int32 GrowingSeasonDaysRemaining = 0;
};
USTRUCT(BlueprintType)
struct FAgrarianGrowingSeasonProfile
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season")
FName TileId = NAME_None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season")
FString GrowingZoneLabel = TEXT("unknown");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season", meta = (ClampMin = "1", ClampMax = "366"))
int32 GrowingSeasonStartDay = 80;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season", meta = (ClampMin = "1", ClampMax = "366"))
int32 GrowingSeasonEndDay = 300;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season", meta = (ClampMin = "0", ClampMax = "366"))
int32 FrostFreeDays = 220;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season")
float MinAverageGrowingTempC = 7.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season", meta = (ClampMin = "0", ClampMax = "90"))
int32 CropSafetyBufferDays = 14;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season")
FString ClimateProfile = TEXT("unknown");
};
USTRUCT(BlueprintType)
struct FAgrarianCropSeasonAssessment
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season")
EAgrarianCropSeasonFit Fit = EAgrarianCropSeasonFit::Unknown;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season")
int32 DaysAvailable = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season")
int32 DaysRequired = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season")
int32 SafetyBufferDays = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season")
bool bCanPlantToday = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season")
FString GrowingZoneLabel = TEXT("unknown");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Growing Season")
FText Reason;
};
USTRUCT(BlueprintType)
struct FAgrarianItemStack
{
@@ -125,6 +231,9 @@ struct FAgrarianSurvivalSnapshot
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival")
float Stamina = 100.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival")
float Exhaustion = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival")
float Hunger = 100.0f;
@@ -136,4 +245,37 @@ struct FAgrarianSurvivalSnapshot
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival")
float InjurySeverity = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Survival")
float SicknessSeverity = 0.0f;
};
USTRUCT(BlueprintType)
struct FAgrarianCareHistorySnapshot
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History")
float NutritionQuality = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History")
float IllnessBurden = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History")
float InjuryBurden = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History")
float SleepQuality = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History")
float ShelterQuality = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History")
float StressBurden = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History")
float WorkloadBurden = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Care History")
float TreatmentQuality = 1.0f;
};