143 lines
4.7 KiB
C++
143 lines
4.7 KiB
C++
// © 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<FName> PauseReasons;
|
||
bool bBegunPlay = false;
|
||
}; |