// © 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:00–20: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); // 0–23 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 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; };