Setup player preview for equipment panel

This commit is contained in:
2026-05-31 15:33:43 +03:00
parent 4218b36ac9
commit 56cc2fce98
42 changed files with 823 additions and 318 deletions
@@ -0,0 +1,264 @@
// © 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();
}
}
}