Files
Naked-Desire/Source/NakedDesire/Global/TimeOfDaySubsystem.h
T

160 lines
5.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// © 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 at 30fps
* (SKY_PUSH_INTERVAL_SECONDS) 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.)
// The skip is animated over SLEEP_TRANSITION_SECONDS — the clock advances a little
// each frame so the sky sweeps smoothly; energy/autosave land when the sweep finishes.
UFUNCTION(BlueprintCallable, Category = "Time")
void Sleep();
// True while the sleep time-lapse is running (BP can hold a dim overlay / lock input).
UFUNCTION(BlueprintPure, Category = "Time")
bool IsSleeping() const { return bSleeping; }
// --- 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:
// bForceSkyPush bypasses the 30fps throttle for discrete jumps (skips / load),
// so the sky snaps to the new time immediately instead of waiting a frame.
void AdvanceClock(double DeltaMinutes, bool bForceSkyPush = false);
// Per-frame driver for the animated sleep sweep; finalizes when the budget is spent.
void TickSleep(float DeltaTime);
void FinishSleep();
void HandleHourBoundary(int32 HourOfDay); // 023
void SetPhase(EDayPhase NewPhase);
void AdvanceCalendarDay();
void ChargeWeeklyRent();
void DepositDailyFollowerIncome();
void PushTimeToSky(bool bForce = false);
static EDayPhase ComputePhase(float InMinuteOfDay);
UGlobalSaveGameData* GetSave() const;
void RestorePlayerEnergy() const;
void Autosave() const;
// Real-time stamp (seconds, FApp clock) of the last sky push, to throttle to 30fps.
double LastSkyPushRealTime = -1.0;
EDayPhase CurrentPhase = EDayPhase::Day;
TSet<FName> PauseReasons;
bool bBegunPlay = false;
// Animated sleep state. While bSleeping, Tick drives the clock at SleepMinutesPerRealSecond
// instead of the normal rate, advancing SleepMinutesRemaining in-game minutes total.
bool bSleeping = false;
double SleepMinutesRemaining = 0.0;
double SleepMinutesPerRealSecond = 0.0;
};