// © 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 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); // 0–23 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 PauseReasons; bool bBegunPlay = false; };