Setup player preview for equipment panel

This commit is contained in:
2026-05-31 15:33:43 +03:00
parent e70b1d757c
commit f59fa8d948
42 changed files with 823 additions and 318 deletions
+13
View File
@@ -7,3 +7,16 @@ inline const FString DefaultSaveSlotName = TEXT("Slot1");
#define IS_DEMO false
#define STARTING_MONEY 1000
// --- Time of day / calendar (GDD §2.4, §10.1; tuning §21) ---
// 1440 in-game minutes elapse over the ~90 real-minute day/night cycle.
inline constexpr float INGAME_MINUTES_PER_REAL_SECOND = 16.0f;
inline constexpr int32 MINUTES_PER_HOUR = 60;
inline constexpr int32 MINUTES_PER_DAY = 1440;
inline constexpr float DAY_START_HOUR = 8.0f; // 08:00 — day phase begins (§10.1)
inline constexpr float NIGHT_START_HOUR = 20.0f; // 20:00 — night phase begins (§10.1)
inline constexpr int32 DAY_ROLL_HOUR = 4; // 04:00 — calendar day increments (§20 #25)
inline constexpr float SLEEP_DURATION_HOURS = 8.0f; // §2.4 sleep fast-forwards 8 hours
inline constexpr int32 CAMPAIGN_LENGTH_DAYS = 90; // §3.3 survive 90 days
inline constexpr int32 WEEK_LENGTH_DAYS = 7; // §2.4 rent due each week
inline constexpr float WEEKLY_RENT = 20000.0f; // §15.3 early-tier placeholder (§21 tuning)
@@ -2,14 +2,9 @@
#include "NakedDesireGameMode.h"
#include "Kismet/GameplayStatics.h"
#include "NakedDesire/Clothing/ClothingItemDefinition.h"
#include "NakedDesire/Clothing/ClothingItemInstance.h"
#include "NakedDesire/Interactables/ItemPickup.h"
#include "UObject/ConstructorHelpers.h"
#include "NakedDesire/Interactables/Wardrobe.h"
#include "NakedDesire/MissionBuilder/MissionsConfig.h"
#include "NakedDesire/MissionBuilder/MissionsManager.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
#include "NakedDesire/SaveGame/ItemSaveRecord.h"
#include "NakedDesire/SaveGame/SaveSubsystem.h"
@@ -19,59 +14,17 @@ void ANakedDesireGameMode::RestartGame()
UGameplayStatics::OpenLevel(this, "City");
}
AWardrobe* ANakedDesireGameMode::GetWardrobe() const
{
return Wardrobe;
}
void ANakedDesireGameMode::BuyItem(UClothingItemInstance* ClothingItemInstance)
{
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
UGlobalSaveGameData* SaveGame = SaveSubsystem->GetCurrentSave();
if (!SaveGame)
{
UE_LOG(LogTemp, Error, TEXT("ANakedDesireGameMode::BuyItem Couldn't load save game"));
return;
}
if (SaveGame->Money < ClothingItemInstance->GetClothingItemDefinition()->BasePrice)
return;
SaveGame->Money -= ClothingItemInstance->GetClothingItemDefinition()->BasePrice;
Wardrobe->AddItem(ClothingItemInstance);
}
void ANakedDesireGameMode::OnHourChanged(int32 Hour)
{
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
UGlobalSaveGameData* SaveGame = SaveSubsystem->GetCurrentSave();
if (!SaveGame)
{
UE_LOG(LogTemp, Error, TEXT("ANakedDesireGameMode::BuyItem Couldn't load save game"));
return;
}
if (Hour == 4)
{
SaveGame->DaysPassed++;
RefreshDailyMissions();
}
}
void ANakedDesireGameMode::BeginPlay()
{
Super::BeginPlay();
if (AActor* FoundActor = UGameplayStatics::GetActorOfClass(GetWorld(), AWardrobe::StaticClass()))
{
if (AWardrobe* WardrobeActor = Cast<AWardrobe>(FoundActor))
{
Wardrobe = WardrobeActor;
}
}
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
for (const FItemSaveRecord& Item : SaveSubsystem->GetCurrentSave()->GetWorldItems())
SpawnSavedWorldItems(SaveSubsystem->GetCurrentSave());
}
void ANakedDesireGameMode::SpawnSavedWorldItems(UGlobalSaveGameData* SaveGameData)
{
for (const FItemSaveRecord& Item : SaveGameData->GetWorldItems())
{
UClothingItemInstance* NewItemInstance = Cast<UClothingItemInstance>(UItemInstance::CreateFromRecord(this, Item));
if (!NewItemInstance)
@@ -81,15 +34,3 @@ void ANakedDesireGameMode::BeginPlay()
NewItemPickup->SetItem(NewItemInstance);
}
}
void ANakedDesireGameMode::RefreshDailyMissions()
{
const ANakedDesireCharacter* Player = Cast<ANakedDesireCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
if (!Player)
return;
USaveSubsystem* SaveSubsystem = UGameplayStatics::GetGameInstance(GetWorld())->GetSubsystem<USaveSubsystem>();
int ClampedIndex = FMath::Clamp(SaveSubsystem->GetCurrentSave()->DaysPassed , 0, MissionsConfig->DailyMissions.Num() - 1);
Player->MissionsManager->RefreshDailyMissions(MissionsConfig->DailyMissions[ClampedIndex].Missions);
}
@@ -6,53 +6,27 @@
#include "GameFramework/GameModeBase.h"
#include "NakedDesireGameMode.generated.h"
class UGlobalSaveGameData;
class AItemPickup;
class UClothingItemInstance;
class UMissionsConfig;
class AWardrobe;
UCLASS(minimalapi)
UCLASS(MinimalAPI)
class ANakedDesireGameMode : public AGameModeBase
{
GENERATED_BODY()
UPROPERTY()
AWardrobe* Wardrobe = nullptr;
UPROPERTY(EditDefaultsOnly)
UMissionsConfig* MissionsConfig;
public:
int NoticeCount = 0;
void RestartGame();
UFUNCTION(BlueprintPure, BlueprintImplementableEvent)
FTimecode GetCurrentTime() const;
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable)
void SetCurrentTime(FTimecode TimeCode);
UFUNCTION(BlueprintPure)
AWardrobe* GetWardrobe() const;
UFUNCTION(BlueprintImplementableEvent)
void EndGameEmbarrassed();
UFUNCTION(BlueprintCallable)
void BuyItem(UClothingItemInstance* ClothingItemInstance);
UFUNCTION(BlueprintCallable)
void OnHourChanged(int32 Hour);
protected:
virtual void BeginPlay() override;
private:
void RefreshDailyMissions();
UPROPERTY(EditDefaultsOnly, Category = "Items")
TSubclassOf<AItemPickup> ItemPickupClass;
void SpawnSavedWorldItems(UGlobalSaveGameData* SaveGameData);
};
@@ -0,0 +1,14 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "PlayerPreviewCaptureSubsystem.h"
void UPlayerPreviewCaptureSubsystem::SetPreviewActive(bool bActive)
{
if (bPreviewActive == bActive)
{
return;
}
bPreviewActive = bActive;
OnPreviewCaptureActiveChanged.Broadcast(bPreviewActive);
}
@@ -0,0 +1,45 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "PlayerPreviewCaptureSubsystem.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPreviewCaptureActiveChanged, bool, bActive);
/**
* Message bus between the equipment/inventory UI (C++) and the player-preview
* sublevel that renders the impostor into a SceneCapture2D for the equipment panel.
*
* The capture is expensive, so it must run only while the panel is open. The preview
* level's Level Blueprint owns the actual 30fps capture loop; it binds to
* OnPreviewCaptureActiveChanged to start/stop that loop. The UI flips the state via
* SetPreviewActive when the inventory screen activates / deactivates.
*
* A world subsystem is shared across the persistent level and its streamed sublevels,
* so the preview level and the main UI talk through the same instance without either
* needing a hard reference to the other.
*/
UCLASS()
class NAKEDDESIRE_API UPlayerPreviewCaptureSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
// Called by the inventory/equipment UI on open (true) and close (false).
UFUNCTION(BlueprintCallable, Category = "Preview")
void SetPreviewActive(bool bActive);
UFUNCTION(BlueprintPure, Category = "Preview")
bool IsPreviewActive() const { return bPreviewActive; }
// Bound by the preview level's Level Blueprint to start/stop its 30fps capture loop.
// Read IsPreviewActive() on BeginPlay to sync initial state in case the panel was
// somehow already open when the sublevel streamed in.
UPROPERTY(BlueprintAssignable, Category = "Preview")
FOnPreviewCaptureActiveChanged OnPreviewCaptureActiveChanged;
private:
bool bPreviewActive = false;
};
@@ -76,6 +76,15 @@ void USessionLossResolver::ResolveLoss(ESessionLossCause Cause)
OnSessionLossResolved.Broadcast(Cause, bWentToHoldingCell);
}
void USessionLossResolver::ResolveSleepLoss()
{
// §4.4: sleeping is the deterministic side of the theft model — everything left
// outside the apartment is lost, regardless of the in-session grace / roll state.
// No autosave here: the sleep flow persists once via UTimeOfDaySubsystem::Sleep(),
// which runs immediately after this. Callers must ensure a save follows.
LoseAllWorldClothing();
}
void USessionLossResolver::LoseAllWorldClothing()
{
UGlobalSaveGameData* Save = GetSave();
@@ -39,6 +39,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "Session")
void ResolveLoss(ESessionLossCause Cause);
// Voluntary sleep at the apartment bed (§4.4 step 2). Not a session-loss cause — the
// session already ended on entering the apartment — but any clothing left outside is
// still guaranteed lost. Lives here so ALL "what gets lost" logic stays in one place.
UFUNCTION(BlueprintCallable, Category = "Session")
void ResolveSleepLoss();
UPROPERTY(BlueprintAssignable, Category = "Session")
FOnSessionLossResolvedSignature OnSessionLossResolved;
@@ -0,0 +1,264 @@
// © 2025 Naked People Team. All Rights Reserved.
#include "TimeOfDaySubsystem.h"
#include "Constants.h"
#include "Kismet/GameplayStatics.h"
#include "Misc/Timecode.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"
#include "NakedDesire/SaveGame/GlobalSaveGameData.h"
#include "NakedDesire/SaveGame/SaveSubsystem.h"
#include "NakedDesire/Stats/StatsManager.h"
void UTimeOfDaySubsystem::OnWorldBeginPlay(UWorld& InWorld)
{
Super::OnWorldBeginPlay(InWorld);
if (const UGlobalSaveGameData* Save = GetSave())
{
CurrentPhase = ComputePhase(Save->MinuteOfDay);
}
bBegunPlay = true;
PushTimeToSky(); // sync the sky to the loaded time immediately
}
void UTimeOfDaySubsystem::Tick(float DeltaTime)
{
if (!bBegunPlay || IsPaused())
return;
AdvanceClock(static_cast<double>(DeltaTime) * INGAME_MINUTES_PER_REAL_SECOND);
}
TStatId UTimeOfDaySubsystem::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(UTimeOfDaySubsystem, STATGROUP_Tickables);
}
int32 UTimeOfDaySubsystem::GetDay() const
{
const UGlobalSaveGameData* Save = GetSave();
return Save ? Save->DaysPassed : 0;
}
float UTimeOfDaySubsystem::GetMinuteOfDay() const
{
const UGlobalSaveGameData* Save = GetSave();
return Save ? Save->MinuteOfDay : 0.0f;
}
int32 UTimeOfDaySubsystem::GetHour() const
{
return FMath::FloorToInt(GetMinuteOfDay() / MINUTES_PER_HOUR);
}
int32 UTimeOfDaySubsystem::GetMinute() const
{
return FMath::FloorToInt(GetMinuteOfDay()) % MINUTES_PER_HOUR;
}
EDayPhase UTimeOfDaySubsystem::GetPhase() const
{
return ComputePhase(GetMinuteOfDay());
}
void UTimeOfDaySubsystem::SkipTime(float Minutes)
{
if (Minutes > 0.0f)
{
AdvanceClock(static_cast<double>(Minutes));
}
}
void UTimeOfDaySubsystem::SkipToNextMorning()
{
const double Target = DAY_START_HOUR * MINUTES_PER_HOUR; // 08:00
double Delta = Target - GetMinuteOfDay();
if (Delta <= 0.0)
{
Delta += MINUTES_PER_DAY;
}
AdvanceClock(Delta);
}
void UTimeOfDaySubsystem::Sleep()
{
SkipTime(SLEEP_DURATION_HOURS * MINUTES_PER_HOUR);
RestorePlayerEnergy();
// TODO(§9.8 / Phase 9): charge the equipped phone to 100% as part of the sleep cycle.
// TODO(§7.3): sleep does NOT reset hunger / effective-max decay — only eating does.
Autosave();
}
void UTimeOfDaySubsystem::PushPause(FName Reason)
{
PauseReasons.Add(Reason);
}
void UTimeOfDaySubsystem::PopPause(FName Reason)
{
PauseReasons.Remove(Reason);
}
void UTimeOfDaySubsystem::AdvanceClock(double DeltaMinutes)
{
UGlobalSaveGameData* Save = GetSave();
if (!Save || DeltaMinutes <= 0.0)
return;
const double Prev = Save->MinuteOfDay;
double Next = Prev + DeltaMinutes;
// Fire an hour boundary for every integer hour crossed (handles midnight wrap and
// multi-hour skips). Each index is folded to 023; boundary 4 rolls the calendar.
const int32 PrevHourIdx = FMath::FloorToInt(Prev / MINUTES_PER_HOUR);
const int32 NextHourIdx = FMath::FloorToInt(Next / MINUTES_PER_HOUR);
for (int32 HourIdx = PrevHourIdx + 1; HourIdx <= NextHourIdx; ++HourIdx)
{
HandleHourBoundary(((HourIdx % 24) + 24) % 24);
}
while (Next >= MINUTES_PER_DAY)
{
Next -= MINUTES_PER_DAY;
}
Save->MinuteOfDay = static_cast<float>(Next);
PushTimeToSky();
}
void UTimeOfDaySubsystem::HandleHourBoundary(int32 HourOfDay)
{
OnHourChanged.Broadcast(HourOfDay);
if (HourOfDay == DAY_ROLL_HOUR)
{
AdvanceCalendarDay();
}
if (HourOfDay == FMath::FloorToInt(DAY_START_HOUR))
{
SetPhase(EDayPhase::Day);
}
else if (HourOfDay == FMath::FloorToInt(NIGHT_START_HOUR))
{
SetPhase(EDayPhase::Night);
}
}
void UTimeOfDaySubsystem::SetPhase(EDayPhase NewPhase)
{
if (NewPhase == CurrentPhase)
return;
CurrentPhase = NewPhase;
OnPhaseChanged.Broadcast(NewPhase);
}
void UTimeOfDaySubsystem::AdvanceCalendarDay()
{
UGlobalSaveGameData* Save = GetSave();
if (!Save)
return;
Save->DaysPassed++;
OnDayChanged.Broadcast(Save->DaysPassed);
DepositDailyFollowerIncome();
if (Save->DaysPassed > 0 && (Save->DaysPassed % WEEK_LENGTH_DAYS) == 0)
{
ChargeWeeklyRent();
}
// §3.3: surviving to day 90 ends the campaign (win). Days advance one at a time, so
// an exact-match fires this once. Endless mode never ends here.
if (!Save->bEndlessMode && Save->DaysPassed == CAMPAIGN_LENGTH_DAYS)
{
OnCampaignEnded.Broadcast(ECampaignEndReason::CampaignComplete);
}
}
void UTimeOfDaySubsystem::ChargeWeeklyRent()
{
UGlobalSaveGameData* Save = GetSave();
if (!Save || Save->bEndlessMode)
return;
if (Save->Money >= WEEKLY_RENT)
{
Save->Money -= WEEKLY_RENT;
Save->LastRentChargeDay = Save->DaysPassed;
Autosave();
}
else
{
// §3.3 / §22 #8: can't make rent → eviction → run over. No grace period.
OnCampaignEnded.Broadcast(ECampaignEndReason::Evicted);
}
}
void UTimeOfDaySubsystem::DepositDailyFollowerIncome()
{
// §7.9 / §20 #25: passive follower income auto-deposits to the bank each day-roll.
// TODO(Phase 8): once a follower-count attribute exists, deposit
// FollowerCount * <daily per-follower rate> into Save->Money here. There is no
// follower attribute yet, so this is intentionally a no-op (payout reads 0).
}
void UTimeOfDaySubsystem::PushTimeToSky()
{
const UGlobalSaveGameData* Save = GetSave();
if (!Save)
return;
const int32 CurMinute = FMath::FloorToInt(Save->MinuteOfDay);
if (CurMinute == LastPushedMinute)
return;
LastPushedMinute = CurMinute;
const int32 Hours = CurMinute / MINUTES_PER_HOUR;
const int32 Minutes = CurMinute % MINUTES_PER_HOUR;
OnPushTimeToSky.Broadcast(FTimecode(Hours, Minutes, 0, 0, false));
}
EDayPhase UTimeOfDaySubsystem::ComputePhase(float InMinuteOfDay)
{
const float Hour = InMinuteOfDay / MINUTES_PER_HOUR;
return (Hour >= DAY_START_HOUR && Hour < NIGHT_START_HOUR) ? EDayPhase::Day : EDayPhase::Night;
}
UGlobalSaveGameData* UTimeOfDaySubsystem::GetSave() const
{
if (const UGameInstance* GameInstance = GetWorld()->GetGameInstance())
{
if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem<USaveSubsystem>())
{
return SaveSubsystem->GetCurrentSave();
}
}
return nullptr;
}
void UTimeOfDaySubsystem::RestorePlayerEnergy() const
{
if (ANakedDesireCharacter* Player = Cast<ANakedDesireCharacter>(UGameplayStatics::GetPlayerCharacter(this, SLOT_PLAYER)))
{
if (Player->StatsManager)
{
Player->StatsManager->RestoreEnergy();
}
}
}
void UTimeOfDaySubsystem::Autosave() const
{
if (const UGameInstance* GameInstance = GetWorld()->GetGameInstance())
{
if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem<USaveSubsystem>())
{
SaveSubsystem->SaveGame();
}
}
}
@@ -0,0 +1,143 @@
// © 2025 Naked People Team. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "TimeOfDaySubsystem.generated.h"
class UGlobalSaveGameData;
/**
* Day vs. night phase (GDD §10.1). Drives NPC density and embarrassment rate.
* Day is 08:0020:00; everything else is night.
*/
UENUM(BlueprintType)
enum class EDayPhase : uint8
{
Day,
Night
};
/**
* Why the 90-day campaign ended (GDD §3.3). Evicted is the rent-failure loss;
* CampaignComplete fires when the player survives to day 90. The ending screen /
* win-threshold logic (§21 open) lives in BP and reacts to OnCampaignEnded.
*/
UENUM(BlueprintType)
enum class ECampaignEndReason : uint8
{
Evicted,
CampaignComplete
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHourChangedSignature, int32, Hour);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDayChangedSignature, int32, NewDay);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPhaseChangedSignature, EDayPhase, NewPhase);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCampaignEndedSignature, ECampaignEndReason, Reason);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPushTimeToSkySignature, const FTimecode&, Timecode);
/**
* The single authoritative clock (GDD §2.4, §10.1). Owns time-of-day and the
* calendar in C++ and pushes the current time to the UltraDynamicSky actor each
* in-game minute via ANakedDesireGameMode::SetCurrentTime — inverting the old
* BP-drives-time flow. Persists to UGlobalSaveGameData (MinuteOfDay / DaysPassed).
*
* The calendar rolls at 04:00 (DAY_ROLL_HOUR); the day phase flips at 08:00 / 20:00.
* Weekly rent is charged every WEEK_LENGTH_DAYS-th roll; follower income deposits
* each roll. Sleep / time-skips funnel through AdvanceClock so boundaries always fire.
*/
UCLASS()
class NAKEDDESIRE_API UTimeOfDaySubsystem : public UTickableWorldSubsystem
{
GENERATED_BODY()
public:
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
// FTickableGameObject
virtual void Tick(float DeltaTime) override;
virtual TStatId GetStatId() const override;
virtual bool IsTickable() const override { return bBegunPlay && !IsTemplate(); }
// --- Queries ---
UFUNCTION(BlueprintPure, Category = "Time")
int32 GetDay() const;
UFUNCTION(BlueprintPure, Category = "Time")
float GetMinuteOfDay() const;
UFUNCTION(BlueprintPure, Category = "Time")
int32 GetHour() const;
UFUNCTION(BlueprintPure, Category = "Time")
int32 GetMinute() const;
UFUNCTION(BlueprintPure, Category = "Time")
EDayPhase GetPhase() const;
UFUNCTION(BlueprintPure, Category = "Time")
bool IsDay() const { return GetPhase() == EDayPhase::Day; }
// --- Time control ---
// Advance the clock by a number of in-game minutes, firing every boundary crossed.
UFUNCTION(BlueprintCallable, Category = "Time")
void SkipTime(float Minutes);
// Fast-forward to the next 08:00 (used by the §4.4 holding-cell cutscene).
UFUNCTION(BlueprintCallable, Category = "Time")
void SkipToNextMorning();
// §2.4 sleep: fast-forward 8 hours, restore energy, autosave. (The apartment bed
// also routes outside-clothing loss through USessionLossResolver — see §4.4.)
UFUNCTION(BlueprintCallable, Category = "Time")
void Sleep();
// --- Pause (reason-keyed; the clock runs only while the reason set is empty).
// Note §11.17: the holding-cell cutscene deliberately does NOT pause the clock. ---
UFUNCTION(BlueprintCallable, Category = "Time")
void PushPause(FName Reason);
UFUNCTION(BlueprintCallable, Category = "Time")
void PopPause(FName Reason);
UFUNCTION(BlueprintPure, Category = "Time")
bool IsPaused() const { return PauseReasons.Num() > 0; }
// --- Delegates ---
UPROPERTY(BlueprintAssignable, Category = "Time")
FOnHourChangedSignature OnHourChanged;
UPROPERTY(BlueprintAssignable, Category = "Time")
FOnDayChangedSignature OnDayChanged;
UPROPERTY(BlueprintAssignable, Category = "Time")
FOnPhaseChangedSignature OnPhaseChanged;
UPROPERTY(BlueprintAssignable, Category = "Time")
FOnCampaignEndedSignature OnCampaignEnded;
UPROPERTY(BlueprintAssignable, Category = "Time")
FOnPushTimeToSkySignature OnPushTimeToSky;
private:
void AdvanceClock(double DeltaMinutes);
void HandleHourBoundary(int32 HourOfDay); // 023
void SetPhase(EDayPhase NewPhase);
void AdvanceCalendarDay();
void ChargeWeeklyRent();
void DepositDailyFollowerIncome();
void PushTimeToSky();
static EDayPhase ComputePhase(float InMinuteOfDay);
UGlobalSaveGameData* GetSave() const;
void RestorePlayerEnergy() const;
void Autosave() const;
// Last whole in-game minute pushed to the sky, to throttle the push to ~1/min.
int32 LastPushedMinute = -1;
EDayPhase CurrentPhase = EDayPhase::Day;
TSet<FName> PauseReasons;
bool bBegunPlay = false;
};