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

264 lines
6.4 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.
#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();
}
}
}